@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,2306 @@
1
+ /**
2
+ * Durable action dispatch at the engine encoded seam.
3
+ *
4
+ * Governing design: `packages/smithers/flows/plan/docs/concepts/step-keys.md`.
5
+ *
6
+ * @since 0.1.0
7
+ */
8
+ import { Sha256 } from "@smthrs/crypto";
9
+ import { FlowEngine } from "@smthrs/engine";
10
+ import { Action, FlowRuntime } from "@smthrs/flow";
11
+ import * as CacheEnvironment from "@smthrs/flow/CacheEnvironment";
12
+ import { Journal, Redaction } from "@smthrs/journal";
13
+ import { Jj } from "@smthrs/kernel";
14
+ import { DerivedKey } from "@smthrs/keys";
15
+ import { AttemptStore, Ownership, RunStore } from "@smthrs/run-store";
16
+ import { CacheStore } from "@smthrs/step-cache";
17
+ import * as Cause from "effect/Cause";
18
+ import * as Clock from "effect/Clock";
19
+ import * as Context from "effect/Context";
20
+ import * as Effect from "effect/Effect";
21
+ import * as Exit from "effect/Exit";
22
+ import * as Metric from "effect/Metric";
23
+ import * as Option from "effect/Option";
24
+ import * as Schema from "effect/Schema";
25
+ import * as EngineStoreMetrics from "../EngineStoreMetrics.js";
26
+ import * as Inconsistency from "../Inconsistency.js";
27
+ import * as ReplayOnly from "../ReplayOnly.js";
28
+ import * as StepBoundary from "../StepBoundary.js";
29
+ import * as StepSandbox from "../StepSandbox.js";
30
+ import * as WorkspaceSandbox from "../WorkspaceSandbox.js";
31
+ import * as AttemptAdmission from "./AttemptAdmission.js";
32
+ import * as CacheAdmission from "./CacheAdmission.js";
33
+ import * as CacheAgeHistory from "./CacheAgeHistory.js";
34
+ import * as CacheAgeVerdicts from "./CacheAgeVerdicts.js";
35
+ import * as CacheOutputPolicy from "./CacheOutputPolicy.js";
36
+ import * as CachePublication from "./CachePublication.js";
37
+ import * as CallFacts from "./CallFacts.js";
38
+ import * as CopiedRecord from "./CopiedRecord.js";
39
+ import * as EffectRecords from "./EffectRecords.js";
40
+ import * as HostReflection from "./HostReflection.js";
41
+ import * as JournalRecords from "./JournalRecords.js";
42
+ import * as ProvenanceSlot from "./ProvenanceSlot.js";
43
+ import * as SandboxedExecution from "./SandboxedExecution.js";
44
+ import * as StepFacts from "./StepFacts.js";
45
+ /**
46
+ * The attempt stopped without settling — it is durably parked, not failed.
47
+ *
48
+ * The distinction matters to the driver: a suspended attempt keeps its row and
49
+ * its attempt number, so resuming continues the same attempt rather than
50
+ * burning a new one against the retry budget.
51
+ *
52
+ * @since 0.1.0
53
+ * @category errors
54
+ */
55
+ export class AttemptSuspended extends Schema.TaggedError()("@smthrs/engine-store/AttemptSuspended", {
56
+ code: Schema.Literal("attempt_suspended"),
57
+ runId: Schema.String,
58
+ keyDigest: Schema.String,
59
+ attempt: Schema.Int
60
+ }) {
61
+ }
62
+ /**
63
+ * The attempt was not admitted, so its body never ran.
64
+ *
65
+ * `outcome` names which admission check refused — a superseded fence, a live
66
+ * same-key attempt, an already-settled row. Because the body did not execute,
67
+ * this failure is always safe to surface without compensation.
68
+ *
69
+ * @since 0.1.0
70
+ * @category errors
71
+ */
72
+ export class AttemptAdmissionRejected extends Schema.TaggedError()("@smthrs/engine-store/AttemptAdmissionRejected", {
73
+ code: Schema.Literal("attempt_admission_rejected"),
74
+ keyDigest: Schema.String,
75
+ outcome: Schema.String
76
+ }) {
77
+ }
78
+ /**
79
+ * Two different runs recorded results under the same step key.
80
+ *
81
+ * The key is a digest of the declaration, so a conflict means the declaration
82
+ * does not fully describe what the step depends on: same key, different
83
+ * answer. `recordedRunId` names the run that got there first, so the
84
+ * divergence can be investigated rather than silently resolved.
85
+ *
86
+ * @since 0.1.0
87
+ * @category errors
88
+ */
89
+ export class CacheConflictDetected extends Schema.TaggedError()("@smthrs/engine-store/CacheConflictDetected", {
90
+ code: Schema.Literal("cache_conflict_detected"),
91
+ keyDigest: Schema.String,
92
+ recordedRunId: Schema.String
93
+ }) {
94
+ }
95
+ /**
96
+ * A cached output's bytes no longer hash to the digest recorded for them.
97
+ *
98
+ * Unlike a succeeded attempt row, a shared cache row is evictable: the entry is
99
+ * dropped and the next dispatch re-executes and re-captures cleanly (issue
100
+ * #164). Reported rather than swallowed so a failing disk is visible.
101
+ *
102
+ * @since 0.1.0
103
+ * @category errors
104
+ */
105
+ export class CacheCorruptionDetected extends Schema.TaggedError()("@smthrs/engine-store/CacheCorruptionDetected", {
106
+ code: Schema.Literal("cache_corruption_detected"),
107
+ keyDigest: Schema.String,
108
+ path: Schema.String,
109
+ recordedDigest: Schema.String,
110
+ measuredDigest: Schema.String
111
+ }) {
112
+ }
113
+ /**
114
+ * Corrupt recorded evidence on a SUCCEEDED durable attempt row under the
115
+ * strict verdict (issue #171).
116
+ *
117
+ * Distinct from {@link CacheCorruptionDetected} on purpose: a corrupt shared
118
+ * cache row is evictable — the next dispatch re-executes and re-captures
119
+ * cleanly (issue #164) — but a succeeded attempt row records that this run's
120
+ * side effects already ran, so eviction/re-execution would violate
121
+ * exactly-once for irreversible actions. The corrupt boundary evidence is
122
+ * quarantined off the succeeded row instead: the driver parks the first
123
+ * detection in the `quarantine` waiting state, and the next explicit resume
124
+ * returns the durable outcome without re-materializing the poisoned evidence
125
+ * or re-executing the action.
126
+ *
127
+ * @since 0.1.0
128
+ * @category errors
129
+ */
130
+ export class AttemptEvidenceQuarantined extends Schema.TaggedError()("@smthrs/engine-store/AttemptEvidenceQuarantined", {
131
+ code: Schema.Literal("attempt_evidence_quarantined"),
132
+ keyDigest: Schema.String,
133
+ attempt: Schema.Int,
134
+ path: Schema.String,
135
+ recordedDigest: Schema.String,
136
+ measuredDigest: Schema.String
137
+ }) {
138
+ }
139
+ /**
140
+ * Extracts an {@link AttemptEvidenceQuarantined} carried anywhere in a flow
141
+ * result's cause — as a typed failure or squashed into a defect — so the
142
+ * driver can park the run instead of settling it `failed` (issue #171).
143
+ *
144
+ * @since 0.1.0
145
+ * @category errors
146
+ */
147
+ export const evidenceQuarantined = (cause) => {
148
+ for (const reason of cause.reasons) {
149
+ const carried = Cause.isFailReason(reason)
150
+ ? reason.error
151
+ : Cause.isDieReason(reason)
152
+ ? reason.defect
153
+ : undefined;
154
+ if (carried instanceof AttemptEvidenceQuarantined) {
155
+ return carried;
156
+ }
157
+ }
158
+ return undefined;
159
+ };
160
+ /**
161
+ * The one classification of a `replayOutputs` failure (issue #150): a
162
+ * `BoundaryCorruption` in the cause is on-disk corruption of recorded
163
+ * evidence — the store's strongest invariant violated — while anything else
164
+ * is a transient host refusal that stays retryable. The two previously
165
+ * journalled identically, so a failing disk corrupting many blobs was
166
+ * indistinguishable from a one-off EIO.
167
+ */
168
+ const replayCorruption = (cause) => {
169
+ for (const reason of cause.reasons) {
170
+ if (Cause.isFailReason(reason) && reason.error instanceof StepBoundary.BoundaryCorruption) {
171
+ return reason.error;
172
+ }
173
+ }
174
+ return undefined;
175
+ };
176
+ /**
177
+ * The stable effect kind a compensation handler is registered under.
178
+ *
179
+ * The action name is the adapter's own identity and is what an engine
180
+ * composition registers its handler by; a dispatch whose action carries no
181
+ * name at all (the plan scheduler's synthetic node dispatch) falls back to a
182
+ * constant, which resolves to no handler and therefore assesses as blocking —
183
+ * the safe direction.
184
+ */
185
+ const actionKind = (action) => typeof action === "object" && action !== null && typeof action.name === "string"
186
+ ? action.name
187
+ : "flows/engine-store/action";
188
+ /**
189
+ * Whether a failing execution failed because it broke its declared boundary.
190
+ *
191
+ * The isolated execution path raises the boundary's own `UndeclaredWrite`, so
192
+ * a violation detected while the body ran classifies exactly like one detected
193
+ * at settle time: the row records `hardViolation` and the journal gets the
194
+ * violation record, rather than the failure passing as an ordinary action
195
+ * error the retry policy might happily retry.
196
+ */
197
+ const declarationViolated = (cause) => cause.reasons.some((reason) => Cause.isFailReason(reason) &&
198
+ (reason.error instanceof StepBoundary.UndeclaredWrite || reason.error instanceof StepSandbox.UndeclaredRead));
199
+ /**
200
+ * Whether a failed settle carries one of the boundary's own contract
201
+ * violations, as opposed to a host refusal. Classification for the
202
+ * `boundarySettlements` counter only — the journal record stays the source
203
+ * of truth.
204
+ */
205
+ const settlementViolated = (cause) => cause.reasons.some((reason) => Cause.isFailReason(reason) &&
206
+ (reason.error instanceof StepBoundary.UndeclaredWrite ||
207
+ reason.error instanceof StepBoundary.MissingDeclaredOutput ||
208
+ reason.error instanceof StepBoundary.SurvivingDeclaredRemoval));
209
+ const AttemptMeta = Schema.Struct({
210
+ tier: Schema.Literals(["sealed", "compensable", "irreversible"]),
211
+ // Only a genuine action idempotency key permits automatic released retry.
212
+ // Old rows without this evidence remain fail-closed (issues #2982/#3409).
213
+ keyed: Schema.optional(Schema.Literal(true)),
214
+ /**
215
+ * The sealed declaration admits multiple legitimate recorded results under
216
+ * this key. Absence remains the durable determinism claim.
217
+ */
218
+ nondeterministic: Schema.optional(Schema.Literal(true)),
219
+ boundary: Schema.optional(StepBoundary.BoundaryEvidence),
220
+ /**
221
+ * The prepare-time measurement matched the caller-declared read set the
222
+ * step key was derived from (issue #106). Only such completions may enter
223
+ * the shared cache: a stale declaration executes against the *real*
224
+ * content, so recording its result under the declaration's key would hand
225
+ * a later, genuinely accurate run the wrong value as a verified hit.
226
+ */
227
+ readSetVerified: Schema.optional(Schema.Literal(true)),
228
+ /**
229
+ * The row's boundary evidence failed integrity verification and was removed
230
+ * after its detailed corruption record reached the journal (issue #171).
231
+ * The succeeded outcome remains authoritative, but this row may never be
232
+ * converged into the shared cache again.
233
+ */
234
+ boundaryQuarantined: Schema.optional(Schema.Literal(true)),
235
+ /**
236
+ * The failed row records a boundary violation (a prepare or settle
237
+ * failure), so the failed replay branch can re-emit the `hardViolation`
238
+ * journal record idempotently after a crash in the finish→emit window
239
+ * (issue #109) — the violation kind is not recoverable from the persisted
240
+ * cause alone.
241
+ */
242
+ hardViolation: Schema.optional(Schema.Literal(true)),
243
+ snapshotId: Schema.optional(Schema.String),
244
+ /**
245
+ * The jj operation that recorded `snapshotId`, when the host reports one.
246
+ * A whole-repository rewind restores it to undo bookmark moves, rebases,
247
+ * `describe`, and `abandon` that a tree restore leaves in place.
248
+ */
249
+ snapshotOperationId: Schema.optional(Schema.String),
250
+ /**
251
+ * The engine `SnapshotBoundary` handle taken before this attempt executed.
252
+ * Wrapped so an opaque `null` or `undefined` handle stays distinguishable
253
+ * from no handle; `handle` is absent when the handle was `undefined`.
254
+ */
255
+ boundarySnapshot: Schema.optional(Schema.Struct({ handle: Schema.optionalKey(Schema.Unknown) })),
256
+ // The incarnation that admitted the running row. Since issues #102/#103
257
+ // the adoption decision rests on the admission permit rather than this
258
+ // nonce — a live same-key fiber of this process would be holding the
259
+ // permit, which distinguishes a dead fiber from a live dispatch in a way
260
+ // the recorded incarnation cannot — but the field is kept as durable
261
+ // forensic evidence of which incarnation last drove the attempt.
262
+ admittedBy: Schema.optional(Ownership.OwnerId),
263
+ /**
264
+ * How far this attempt's effect boundary got, in EXECUTABLE state.
265
+ *
266
+ * The journal's boundary records already say this, and adoption cannot use
267
+ * them: `@smthrs/journal` puts every payload it writes through redaction,
268
+ * deliberately, so a recorded `output` may never be re-entered as a flow's
269
+ * result (issue #72), and the journal offers no read addressed by producer
270
+ * identity — only a full scan of the run. So the same fact is written here,
271
+ * where the attempt's `outcome` column is already replayed verbatim, in the
272
+ * SAME transaction as the record that describes it.
273
+ *
274
+ * - `intended`: the body was dispatched and its outcome is not durably
275
+ * known. Whether the effect reached the outside world is genuinely
276
+ * unanswerable from durable state.
277
+ * - `parked`: the body reached a durable wait and suspended, so it never
278
+ * finished and nothing crossed. Recorded explicitly because a park leaves
279
+ * the same `running` row a crash does.
280
+ * - `succeeded`: the body completed and the row carries its result.
281
+ */
282
+ effectCrossing: Schema.optional(Schema.Literals(["intended", "parked", "succeeded"]))
283
+ });
284
+ const decodeMeta = (value) => {
285
+ const decoded = Schema.decodeUnknownResult(AttemptMeta)(value);
286
+ return decoded._tag === "Success" ? decoded.success : undefined;
287
+ };
288
+ /**
289
+ * The earliest boundary snapshot handle persisted for an action key's
290
+ * surviving attempts, walking `earliest` through `latest`. An attempt row
291
+ * without one (it crashed before its snapshot was persisted) is skipped.
292
+ *
293
+ * @since 0.1.0
294
+ * @category combinators
295
+ */
296
+ export const earliestBoundarySnapshot = (attempts, runId, stepKeyDigest, range) => Effect.gen(function* () {
297
+ for (let attempt = range.earliest; attempt <= range.latest; attempt++) {
298
+ const row = yield* attempts.get({ runId, stepKeyDigest, attempt }).pipe(Effect.orDie);
299
+ const persisted = Option.isSome(row) ? decodeMeta(row.value.meta)?.boundarySnapshot : undefined;
300
+ if (persisted !== undefined)
301
+ return Option.some(persisted.handle);
302
+ }
303
+ return Option.none();
304
+ });
305
+ const CauseJson = Schema.Struct({
306
+ reasons: Schema.Array(Schema.Union([
307
+ Schema.Struct({ _tag: Schema.Literal("Fail"), error: Schema.Unknown }),
308
+ Schema.Struct({ _tag: Schema.Literal("Die"), defect: Schema.Unknown }),
309
+ Schema.Struct({
310
+ _tag: Schema.Literal("Interrupt"),
311
+ fiberId: Schema.optional(Schema.NullOr(Schema.Number))
312
+ })
313
+ ]))
314
+ });
315
+ /**
316
+ * Maximum nesting copied from one live failure value.
317
+ *
318
+ * The bounds here are the store's own admission limits, less the envelope
319
+ * `persistCause` wraps around a reduced value, and never tighter: a value the
320
+ * store would have persisted must not be thrown away here. `AttemptStore`
321
+ * admits 128 levels, and a reduced value sits three levels down, inside
322
+ * `{ reasons: [{ error }] }`.
323
+ *
324
+ * @since 1.0.0
325
+ * @category constants
326
+ */
327
+ export const maxInertJsonDepth = AttemptStore.maximumJsonDepth - 4;
328
+ /**
329
+ * Maximum JSON values copied from one persisted cause.
330
+ *
331
+ * The budget is spent across the cause object, its reasons array, every reason
332
+ * envelope, and every reduced leaf. A cause that cannot fit collapses to one
333
+ * bounded reason before it reaches `AttemptStore`. Own-key enumeration has a
334
+ * separate allowance of the same size so ignored keys cannot make traversal
335
+ * unbounded.
336
+ *
337
+ * @since 1.0.0
338
+ * @category constants
339
+ */
340
+ export const maxInertJsonNodes = AttemptStore.maximumJsonNodes - 8;
341
+ /**
342
+ * Maximum UTF-16 units copied from one persisted cause, keys included.
343
+ *
344
+ * Spent across every caller-supplied key and string leaf. This bounds text the
345
+ * reduction inspects and retains, not the serialized byte length: numeric
346
+ * spellings, punctuation, and the fixed cause envelope are outside this
347
+ * character count and are bounded separately by {@link maxInertJsonNodes}.
348
+ *
349
+ * @since 1.0.0
350
+ * @category constants
351
+ */
352
+ export const maxInertJsonCharacters = 256 * 1024;
353
+ const inertJsonRejected = Symbol("inertJsonRejected");
354
+ const inertJsonOmitted = Symbol("inertJsonOmitted");
355
+ const arrayBufferViewMarker = "[ArrayBufferView]";
356
+ /** Charges JSON values against the cause-wide node allowance. */
357
+ const spendNodes = (budget, nodes) => {
358
+ if (budget.nodes + nodes > maxInertJsonNodes)
359
+ return false;
360
+ budget.nodes += nodes;
361
+ return true;
362
+ };
363
+ /** The store rejects unpaired surrogates, so an ill-formed string is refused. */
364
+ const isWellFormedInertString = (value) => {
365
+ for (let index = 0; index < value.length; index++) {
366
+ const unit = value.charCodeAt(index);
367
+ if (unit >= 0xd800 && unit <= 0xdbff) {
368
+ const low = value.charCodeAt(++index);
369
+ if (!(low >= 0xdc00 && low <= 0xdfff))
370
+ return false;
371
+ }
372
+ else if (unit >= 0xdc00 && unit <= 0xdfff)
373
+ return false;
374
+ }
375
+ return true;
376
+ };
377
+ /**
378
+ * Charges one string, a key or a value, against the cause's character budget.
379
+ * Answers whether the string is admitted.
380
+ */
381
+ const spend = (budget, value) => {
382
+ budget.characters += value.length;
383
+ return budget.characters <= maxInertJsonCharacters && isWellFormedInertString(value);
384
+ };
385
+ /** Charges every own key before its type or descriptor can make it ignorable. */
386
+ const spendEnumeratedKey = (budget, key) => {
387
+ budget.enumeratedKeys += 1;
388
+ if (budget.enumeratedKeys > maxInertJsonNodes)
389
+ return false;
390
+ return typeof key !== "string" || spend(budget, key);
391
+ };
392
+ /**
393
+ * Projects one failure value into the inert JSON data `AttemptStore` admits.
394
+ *
395
+ * A `Fail` reason raised by a service rather than by `Action.executeEncoded`
396
+ * carries a live class instance — `StepBoundary.UnsupportedBoundary`, a
397
+ * `StepSandbox` refusal, any `Data.TaggedError` — and a `Die` reason carries
398
+ * an arbitrary defect. `@smthrs/run-store`'s attempt boundary refuses a
399
+ * non-plain object outright rather than serializing it, so the projection has
400
+ * to happen on the write side, here, where the durable shape is already owned.
401
+ *
402
+ * The reduction reads only own data-property descriptors. It never invokes a
403
+ * getter or `toJSON`, and a host-detected proxy is rejected before any of its
404
+ * traps can run. An array-buffer view becomes the fixed
405
+ * `"[ArrayBufferView]"` marker before its indexed own keys can be materialized;
406
+ * its bytes and attached properties are deliberately omitted. JSON leaves,
407
+ * arrays, and enumerable string-keyed object data otherwise keep their ordinary
408
+ * JSON shape. Unsupported object members are omitted and unsupported array
409
+ * members become `null`, matching JSON's container rules. Every key returned
410
+ * by an own-key enumeration spends from a cause-wide allowance before its type,
411
+ * enumerability, or descriptor is inspected.
412
+ *
413
+ * Depth, node, and character bounds apply before the attempt store's admission
414
+ * boundary, and `budget` carries the allowances across every reason of one
415
+ * cause. The caller charges the root value as part of its reason envelope; this
416
+ * walk charges every descendant. A cycle, proxy, inspection refusal, or
417
+ * exceeded bound rejects the whole value as `null`, so persistence on the
418
+ * failure path neither executes user code nor leaves an attempt running because
419
+ * its error was too large for the store.
420
+ */
421
+ const inertJson = (value, budget) => {
422
+ const active = new WeakSet();
423
+ const reduce = (current, depth) => {
424
+ if (depth > maxInertJsonDepth)
425
+ return inertJsonRejected;
426
+ if (depth > 0 && !spendNodes(budget, 1))
427
+ return inertJsonRejected;
428
+ if (current === null)
429
+ return null;
430
+ switch (typeof current) {
431
+ case "boolean":
432
+ return current;
433
+ case "number":
434
+ return Number.isFinite(current) ? current : null;
435
+ case "string":
436
+ return spend(budget, current) ? current : inertJsonRejected;
437
+ case "undefined":
438
+ case "symbol":
439
+ case "function":
440
+ case "bigint":
441
+ return inertJsonOmitted;
442
+ case "object": {
443
+ if (HostReflection.host.isProxy(current) || active.has(current))
444
+ return inertJsonRejected;
445
+ if (ArrayBuffer.isView(current))
446
+ return arrayBufferViewMarker;
447
+ active.add(current);
448
+ try {
449
+ if (Array.isArray(current)) {
450
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(current, "length");
451
+ /* v8 ignore next 3 -- A non-proxy Array always has an own data-property uint32 length. */
452
+ if (lengthDescriptor === undefined || !("value" in lengthDescriptor) ||
453
+ typeof lengthDescriptor.value !== "number")
454
+ return inertJsonRejected;
455
+ const output = [];
456
+ for (let index = 0; index < lengthDescriptor.value; index++) {
457
+ const descriptor = Object.getOwnPropertyDescriptor(current, String(index));
458
+ const reduced = reduce(descriptor !== undefined && "value" in descriptor ? descriptor.value : undefined, depth + 1);
459
+ if (reduced === inertJsonRejected)
460
+ return inertJsonRejected;
461
+ output.push(reduced === inertJsonOmitted ? null : reduced);
462
+ }
463
+ return output;
464
+ }
465
+ const output = {};
466
+ // This engine-owned error's native Error.message is non-enumerable,
467
+ // but is part of the durable schema used to classify a landing
468
+ // conflict. Preserve that one data field, never getters, stacks, or
469
+ // arbitrary hidden properties. It spends the same bounded budget.
470
+ const tag = Object.getOwnPropertyDescriptor(current, "_tag");
471
+ const conflictMessage = tag !== undefined && "value" in tag &&
472
+ tag.value === WorkspaceSandbox.MaterializationConflict.identifier;
473
+ for (const key of Reflect.ownKeys(current)) {
474
+ if (!spendEnumeratedKey(budget, key))
475
+ return inertJsonRejected;
476
+ if (typeof key !== "string")
477
+ continue;
478
+ const descriptor = Object.getOwnPropertyDescriptor(current, key);
479
+ /* v8 ignore next -- A key returned by Reflect.ownKeys exists on a non-proxy object. */
480
+ if (descriptor === undefined)
481
+ continue;
482
+ if ((!descriptor.enumerable && !(key === "message" && conflictMessage)) || !("value" in descriptor)) {
483
+ continue;
484
+ }
485
+ // `message` and `stack` are observability text, never a field replay
486
+ // classifies on, and a git or HTTP failure embeds its remote URL,
487
+ // credentials included. They meet the journal's own rules; every
488
+ // other member, `_tag` included, stays byte-exact for replay.
489
+ const member = (key === "message" || key === "stack") && typeof descriptor.value === "string"
490
+ ? Redaction.redact(descriptor.value)
491
+ : descriptor.value;
492
+ const reduced = reduce(member, depth + 1);
493
+ if (reduced === inertJsonRejected)
494
+ return inertJsonRejected;
495
+ if (reduced === inertJsonOmitted)
496
+ continue;
497
+ Object.defineProperty(output, key, {
498
+ value: reduced,
499
+ enumerable: true,
500
+ configurable: true,
501
+ writable: true
502
+ });
503
+ }
504
+ return output;
505
+ }
506
+ finally {
507
+ active.delete(current);
508
+ }
509
+ }
510
+ }
511
+ /* v8 ignore next -- The typeof switch covers every JavaScript value category. */
512
+ return inertJsonRejected;
513
+ };
514
+ try {
515
+ const reduced = reduce(value, 0);
516
+ return reduced === inertJsonRejected || reduced === inertJsonOmitted ? null : reduced;
517
+ }
518
+ catch {
519
+ return null;
520
+ }
521
+ };
522
+ /**
523
+ * Encodes a live `Cause` into the plain tagged-reason JSON `rehydrateCause`
524
+ * decodes. Persisting the `Cause` object itself left the durable shape to
525
+ * whatever the ambient serializer produced (`Cause.toJSON` emits
526
+ * `{_id, failures}`, a structural walk emits `{reasons}`), so a change in
527
+ * the store's encoding silently broke failed-attempt replay. The write side
528
+ * now owns the shape explicitly. {@link inertJson} makes every live leaf
529
+ * inert and bounded before the store sees it, against one budget shared by
530
+ * the fixed envelope and every reason. The loop stops at the first reason that
531
+ * cannot fit and returns one `Die(null)` reason, which {@link rehydrateCause}
532
+ * decodes on replay, so even an adversarial reason count remains admissible.
533
+ */
534
+ const persistCause = (cause) => {
535
+ // The cause object and its `reasons` array are the two fixed envelope nodes.
536
+ const budget = { nodes: 2, characters: 0, enumeratedKeys: 0 };
537
+ const reasons = [];
538
+ for (const reason of cause.reasons) {
539
+ // One object, its `_tag` string, and its error, defect, or fiber-id value.
540
+ if (!spendNodes(budget, 3)) {
541
+ return { reasons: [{ _tag: "Die", defect: null }] };
542
+ }
543
+ if (Cause.isFailReason(reason)) {
544
+ reasons.push({ _tag: "Fail", error: inertJson(reason.error, budget) });
545
+ }
546
+ else if (Cause.isDieReason(reason)) {
547
+ reasons.push({ _tag: "Die", defect: inertJson(reason.defect, budget) });
548
+ }
549
+ else {
550
+ reasons.push({ _tag: "Interrupt", fiberId: reason.fiberId ?? null });
551
+ }
552
+ }
553
+ return { reasons };
554
+ };
555
+ /**
556
+ * Presents a payload this module already reduced to JSON as the attempt
557
+ * store's `JsonValue`.
558
+ *
559
+ * `@smthrs/run-store` types the `meta`, `error`, and `outcome` columns as
560
+ * `JsonValue` and admits every write through its own inert-JSON boundary,
561
+ * which refuses anything that boundary rejects with `AttemptStoreError`. The
562
+ * values handed here are already reduced: `AttemptMeta` is a schema whose
563
+ * optional fields are omitted rather than written as `undefined`,
564
+ * {@link persistCause} passes every leaf through {@link inertJson}, and an
565
+ * action `outcome` is the encoded result the caller's own codec produced.
566
+ * TypeScript still refuses the assignment because an optional property's type
567
+ * includes `undefined` and a JSON index signature does not, and because a
568
+ * schema-level `unknown` leaf cannot be proven JSON statically. The store's
569
+ * admission is the check; this is the type-level statement of what it checks.
570
+ */
571
+ const attemptPayload = (value) => value;
572
+ /**
573
+ * Rebuilds the persisted failure of a `failed` attempt row so replay can
574
+ * rethrow the original domain error (issue #59). The row's `error` column
575
+ * holds the {@link persistCause} encoding of the failing `Cause` — a plain
576
+ * object whose `reasons` carry the tagged `Fail`/`Die`/`Interrupt` material.
577
+ * `Fail` errors are already schema-encoded by `Action.executeEncoded`, so
578
+ * their `_tag` survives and `RetryPolicy` non-retryable matching applies on
579
+ * replay exactly as it did on the live attempt. Live reasons are rebuilt
580
+ * unconditionally from the tagged material; anything unrecognizable becomes
581
+ * a defect carrying the raw persisted value.
582
+ */
583
+ const rehydrateCause = (error) => {
584
+ const decoded = Schema.decodeUnknownResult(CauseJson)(error);
585
+ if (decoded._tag === "Success" && decoded.success.reasons.length > 0) {
586
+ return Cause.fromReasons(decoded.success.reasons.map((reason) => {
587
+ switch (reason._tag) {
588
+ case "Fail":
589
+ return Cause.makeFailReason(reason.error);
590
+ case "Die":
591
+ return Cause.makeDieReason(reason.defect);
592
+ case "Interrupt":
593
+ return Cause.makeInterruptReason(reason.fiberId ?? undefined);
594
+ }
595
+ }));
596
+ }
597
+ return Cause.die(error);
598
+ };
599
+ /**
600
+ * The {@link module:CacheEnvironment.CachePolicy} the dispatched declaration
601
+ * annotated, if any.
602
+ *
603
+ * The action stays opaque to this module — it is `unknown` on
604
+ * {@link ActionInput} on purpose — so the policy is read out of its annotation
605
+ * bag rather than off a field. An action without one, or a caller that hands
606
+ * the seam a bare object (every unit test of this module does), simply has no
607
+ * policy and keeps the pre-policy behavior.
608
+ *
609
+ * @since 0.1.0
610
+ * @private
611
+ */
612
+ const declaredCachePolicy = (action) => {
613
+ const annotations = action?.annotations;
614
+ return Context.isContext(annotations) ? CacheEnvironment.cachePolicyOf(annotations) : undefined;
615
+ };
616
+ /**
617
+ * Constructs the encoded action executor. The action itself stays opaque;
618
+ * the supplied dispatcher is the only physical execution point.
619
+ *
620
+ * @since 0.1.0
621
+ * @category constructors
622
+ */
623
+ export const make = (deps) => {
624
+ /**
625
+ * Runs one action body, and tells whoever drove this dispatch that it ran.
626
+ *
627
+ * The report is the only way a node above this knows the difference between
628
+ * work and replay. Every execution path funnels through here, so a body
629
+ * that starts says so exactly once; a path that returns a durable record
630
+ * instead reports `replayed` at its own return. A dispatch that reports
631
+ * nothing is read as work, which is the safe reading: a node never claims
632
+ * it rebuilt nothing when it might have rebuilt something.
633
+ *
634
+ * The digest travels with the report because it is what joins the two
635
+ * halves of a node's evidence: the attempt records this dispatch writes are
636
+ * keyed by it, and the node record above has no other way to name them.
637
+ */
638
+ const execute = (input, stepKeyDigest) => Effect.andThen(Action.reportDispatch({ outcome: "executed", stepKeyDigest, attempt: input.attempt }), deps.execute(input));
639
+ const admission = deps.admission ?? AttemptAdmission.makeUnsafe();
640
+ const recordedAgeVerdict = deps.cacheAgeVerdict ?? CacheAgeVerdicts.make(deps.runId);
641
+ // The lineage every record this executor writes addresses itself to.
642
+ // An action is a node inside its run's root lineage, not a lineage of
643
+ // its own: a lineage segment is minted only where a separate run is.
644
+ const lineageId = FlowEngine.Lineage.root(deps.runId);
645
+ return Effect.fn("ActionPersistence.execute")((input) => {
646
+ // What a replay-only engine reports for this dispatch: the digest once it
647
+ // exists, and whether the dispatch stopped at the execution gate.
648
+ const replay = { gated: false };
649
+ return Effect.gen(function* () {
650
+ yield* Effect.annotateCurrentSpan({
651
+ runId: deps.runId,
652
+ attempt: input.attempt,
653
+ tier: input.tier
654
+ });
655
+ const attempts = yield* AttemptStore.AttemptStore;
656
+ const cache = yield* CacheStore.CacheStore;
657
+ const journal = yield* Journal.Journal;
658
+ const runs = yield* RunStore.RunStore;
659
+ const policy = declaredCachePolicy(input.action);
660
+ // SCOPE NARROWS THE CACHE ADDRESS. The engine derives a cross-run key
661
+ // from the inputs and the environment, which is the widest reach a
662
+ // content address can have. A declaration that wants a narrower one
663
+ // folds the identity it is narrowing to into the address of its cache
664
+ // row, so a sibling run derives a different address and never finds the
665
+ // row: narrowing by construction rather than by a read-time filter that
666
+ // a second reader could disagree with. `shared` is the unnarrowed
667
+ // default and folds nothing, so every key predating the policy keeps
668
+ // its bytes.
669
+ const scopeTag = policy?.scope === "run"
670
+ ? `run:${deps.runId}`
671
+ : policy?.scope === "flow"
672
+ // The flow tag comes from the executing instance, which the engine
673
+ // provides around every dispatch. A caller that reaches this seam
674
+ // without one — the unit tests of this module — cannot name a flow, so
675
+ // the key narrows to the run instead: never wider than the caller
676
+ // asked for.
677
+ ? `flow:${Option.getOrUndefined(Option.map(yield* Effect.serviceOption(FlowRuntime.FlowInstance), (instance) => instance.flow._tag)) ?? `run:${deps.runId}`}`
678
+ : undefined;
679
+ // THE STEP KEY DIGEST IS NOT NARROWED. It is the identity the rest of the
680
+ // engine addresses this dispatch by: `flows_attempts` rows, which
681
+ // `EngineStore.actionRetryOrigin` and `actionLatestAttempt` probe under
682
+ // `sha256(input.key)`, and the `stepKeyDigest` on every attempt record,
683
+ // which `PlanScheduler` maps back to the node that dispatched it. Both
684
+ // derive the digest from the key alone and neither can see an
685
+ // annotation, so narrowing this one would hide a scoped step's attempts
686
+ // from its own retry counter and its deviations from the reconciler.
687
+ const stepKeyDigest = yield* Schema.decodeUnknownEffect(Sha256)(input.key).pipe(Effect.orDie);
688
+ replay.stepKeyDigest = stepKeyDigest;
689
+ // What the scope narrows is the CACHE ROW ADDRESS, which is the only
690
+ // thing a scope is about: an unscoped declaration addresses the row by
691
+ // the step key alone, and a narrowed one folds the identity it is
692
+ // narrowing to into the address, so a sibling derives a different one and
693
+ // never finds the row. The two digests are the same string for every
694
+ // declaration that narrows nothing.
695
+ const keyDigest = scopeTag === undefined
696
+ ? stepKeyDigest
697
+ : yield* Schema.decodeUnknownEffect(Sha256)(`${input.key}\u0000scope:${scopeTag}`).pipe(Effect.orDie);
698
+ yield* Effect.annotateCurrentSpan({ stepKeyDigest, keyDigest });
699
+ const attemptId = { runId: deps.runId, stepKeyDigest, attempt: input.attempt };
700
+ /**
701
+ * Lifecycle events take the journal's durable channel, fenced to the
702
+ * owning process: the write commits inside the SQL sink while
703
+ * `flows_runs` still records `deps.owner`, so a reclaimed (zombie) run
704
+ * fails with `fence_lost` — surfaced as self-interruption, matching the
705
+ * store-level fence outcomes — and a saturated lossy queue can never
706
+ * drop an attempt lifecycle record (issue #10).
707
+ */
708
+ const emitLifecycle = (record) => journal.emitDurable(record, deps.owner).pipe(Effect.catch((error) => error.code === "fence_lost" ? Effect.interrupt : Effect.fail(error)));
709
+ const callFacts = CallFacts.make(input.action, deps.runId);
710
+ const stepFacts = StepFacts.make(input.action, deps.runId);
711
+ const emitStepSettled = (outcome) => Effect.flatMap(stepFacts.settled(outcome), (record) => record === undefined ? Effect.void : Effect.asVoid(emitLifecycle(record)));
712
+ const emitCallInvoked = Effect.flatMap(callFacts.invoked, (record) => record === undefined ? Effect.void : Effect.asVoid(emitLifecycle(record)));
713
+ const emitCallSettled = (outcome) => Effect.flatMap(callFacts.settled(outcome), (record) => record === undefined ? Effect.void : Effect.asVoid(emitLifecycle(record)));
714
+ /**
715
+ * Commits an attempt/cache state transition and the lifecycle records
716
+ * describing it in ONE write transaction.
717
+ *
718
+ * The store writes below (`attempts.put`/`patch`/`finish`, `cache.put`)
719
+ * and `emitLifecycle` all go through the same `DurableWriter`, so the inner
720
+ * writes become savepoints of this transaction: either the row and its
721
+ * journal entry are both durable, or neither is. Before this, a crash in
722
+ * the interstitial left an attempt row the journal could not explain —
723
+ * `attemptStarted` with no `attemptFinished`, forever. Temporal's
724
+ * mutable state and history events commit as one persistence request
725
+ * (`reference/temporal/service/history/workflow/transaction_impl.go`);
726
+ * this is that unit of work, scoped to one attempt.
727
+ *
728
+ * Nothing that is not storage work belongs inside: the action body,
729
+ * the Jj snapshot, and the boundary prepare/settle all stay outside so
730
+ * the write transaction is never held across a host call.
731
+ */
732
+ const atomically = (effect) => journal.transact(effect);
733
+ /**
734
+ * Commits an attempt's terminal row and the lifecycle records describing
735
+ * it as one unit, reporting whether the fenced write landed.
736
+ *
737
+ * Returning a boolean rather than interrupting inside keeps the
738
+ * transaction's failure paths to genuine storage failures: a lost fence
739
+ * is an ordinary "someone else owns this attempt now" outcome, and its
740
+ * self-interruption belongs to the caller, outside the transaction.
741
+ */
742
+ const settleAttempt = (row, records) => atomically(Effect.gen(function* () {
743
+ const finished = yield* attempts.finish(row, deps.owner);
744
+ if (finished._tag !== "Finished") {
745
+ return false;
746
+ }
747
+ // The checkpoint fact precedes its completion boundary so a rewind
748
+ // retaining that completion also retains the observation it owns.
749
+ if (row.state === "succeeded" && stepFacts.annotated) {
750
+ const committed = yield* attempts.get(attemptId);
751
+ if (Option.isNone(committed)) {
752
+ return yield* Effect.fail(new AttemptStore.AttemptStoreError({
753
+ code: "persistence_failed",
754
+ method: "get",
755
+ message: "Finished checkpoint outcome disappeared",
756
+ cause: attemptId
757
+ }));
758
+ }
759
+ yield* emitStepSettled(committed.value.outcome);
760
+ }
761
+ yield* Effect.forEach(records, (record) => emitLifecycle(record), { discard: true });
762
+ if (row.state === "succeeded" && callFacts.settles) {
763
+ const committed = yield* attempts.get(attemptId);
764
+ if (Option.isNone(committed)) {
765
+ return yield* Effect.die(new Error("Finished call outcome disappeared"));
766
+ }
767
+ yield* emitCallSettled(committed.value.outcome);
768
+ }
769
+ return true;
770
+ }));
771
+ const fencedAtMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor));
772
+ const heartbeat = yield* runs.heartbeat(deps.runId, deps.owner, fencedAtMs);
773
+ if (heartbeat._tag !== "Updated") {
774
+ return yield* Effect.interrupt;
775
+ }
776
+ if (input.tier === "irreversible" && input.attempt > 1 && deps.idempotencyKey === undefined) {
777
+ return yield* Effect.fail(new Action.IrreversibleRetryRequiresIdempotencyKey({
778
+ actionName: actionKind(input.action),
779
+ attempt: input.attempt
780
+ }));
781
+ }
782
+ // A source glob is cacheable only after the scheduler pins and replaces
783
+ // it with exact measured inputs. Direct actions retain the pattern, so
784
+ // their key cannot prove which expansion it names and must stay local.
785
+ const cacheDeclaration = CacheAdmission.declaration(input);
786
+ const declarationMeta = {
787
+ tier: input.tier,
788
+ ...(deps.idempotencyKey === undefined ? {} : { keyed: true }),
789
+ ...(input.nondeterministic === undefined ? {} : { nondeterministic: input.nondeterministic })
790
+ };
791
+ /**
792
+ * Cache-provenance producer identity (issue #124): the plain
793
+ * `{runId, sourceId}` identity carried no `sourceSeq`, so every re-drive of
794
+ * the same observation — the same replay refusal against the same
795
+ * recorded row, on every later dispatch of the key — appended a fresh
796
+ * journal row forever. A per-(key, action, recorded-provenance)
797
+ * identity with `sourceSeq 0` collapses the identical re-observation
798
+ * into a `Duplicate`, while folding the recorded row's provenance into
799
+ * the identity keeps a genuinely new observation — the same action
800
+ * against a *different* recorded row — a distinct producer that still
801
+ * journals.
802
+ *
803
+ * The identity names the run, the step-key digest, the decision, and the
804
+ * recorded row's provenance — everything the decision is ABOUT — and
805
+ * deliberately nothing about WHO TOOK IT. `deps.sourceId` is the host's
806
+ * configured `journalSource` (`EngineStore.Options.journalSource`), which
807
+ * a run outlives: the process that admits a row at 900 ms is routinely
808
+ * not the process that resumes the run at 1100 ms. Folding it in made the
809
+ * `ttl` verdict below re-decidable — the resuming incarnation got
810
+ * `Accepted` for a fresh verdict under its own identity and re-judged the
811
+ * age against its own clock, which is the one thing a recorded verdict
812
+ * exists to prevent — and made every other record here re-appendable once
813
+ * per incarnation rather than once per observation.
814
+ *
815
+ * The sibling `recorded` identity below still carries `deps.sourceId`, on
816
+ * purpose: nothing reads a decision back out of it. It exists so a
817
+ * convergence re-record collapses into a `Duplicate`, and a resume under
818
+ * another journal source costs one extra provenance row there, never a
819
+ * different answer.
820
+ */
821
+ const cacheSource = (action, recorded) => ({
822
+ runId: deps.runId,
823
+ lineageId,
824
+ sourceId: `cache:${keyDigest}:${action}${recorded === undefined ? "" : `:${recorded.runId}:${recorded.eventSeq}`}`,
825
+ sourceSeq: 0
826
+ });
827
+ const noteUnshareable = (unshareable) => Effect.gen(function* () {
828
+ const reason = yield* Schema.decodeUnknownEffect(Sha256)(`${unshareable.stage}:${unshareable.message}`).pipe(Effect.orDie);
829
+ yield* emitLifecycle(JournalRecords.cacheProvenance(cacheSource(`unpublished:${reason}`), {
830
+ keyDigest,
831
+ action: "unpublished",
832
+ stage: unshareable.stage,
833
+ message: unshareable.message
834
+ }));
835
+ });
836
+ /**
837
+ * Records the sealed completion into the shared cache with provenance,
838
+ * failing (by default) on a divergent first-recorded row. Used by both
839
+ * the fresh completion path and succeeded-attempt replay, so a crash
840
+ * between `attempts.finish` and `cache.put` converges on restart
841
+ * instead of leaving the cache permanently behind the journal.
842
+ */
843
+ const recordCache = (options) => Effect.gen(function* () {
844
+ // BLOBS BEFORE METADATA (issue #172). Every artifact this evidence
845
+ // references is made durable in the shared tier *before* the entry
846
+ // that references it is written, so a sibling machine can never see
847
+ // a hit whose outputs it cannot materialize. Bazel's REAPI ordering
848
+ // constraint, stated at `UploadManifest.java:630-633`; the whole
849
+ // protocol lives in `CachePublication`, which is a no-op when no
850
+ // shared tier is configured. It runs OUTSIDE the write transaction
851
+ // below, like every other host call.
852
+ //
853
+ // A refusal withholds the SHARED entry, never the local row and never
854
+ // the run: this point is reached after `attempts.finish`, so the work
855
+ // is already done and durably recorded, and failing here would throw
856
+ // a real result away because an optional accelerator was unreachable.
857
+ // It is journalled below instead — the same "visible, not silent"
858
+ // treatment an unverified read set gets (issue #106).
859
+ const meta = options.admission.meta;
860
+ let unshareable = yield* CachePublication.publishArtifacts(meta.boundary);
861
+ // The producer identity folds a digest of the recorded content
862
+ // (issue #129): a constant per-key identity made a post-eviction
863
+ // re-record collapse into a `Duplicate` carrying the EVICTED
864
+ // generation's seq, so the fresh row inherited the evicted row's
865
+ // exact provenance and a laggard's #119 `ifRecordedBy` fence could
866
+ // delete the valid new row. With the content folded in, the #124
867
+ // convergence re-record (identical content, rebuilt from the same
868
+ // persisted attempt row, so the serialization is byte-stable)
869
+ // still collapses into a `Duplicate` whose receipt carries the
870
+ // original emission's canonical seq, while a re-record with
871
+ // different content is a distinct producer that journals fresh
872
+ // provenance. A re-record whose content happens to equal the
873
+ // evicted generation's shares its provenance by design: the two
874
+ // rows are indistinguishable, so a fenced evict of one is exactly
875
+ // a fenced evict of the other.
876
+ //
877
+ // The digest goes through `DerivedKey`, the repo's one hashing
878
+ // chokepoint: RFC 8785 canonical JSON, then SHA-256. Hashing
879
+ // `JSON.stringify` output made "byte stable" depend on key order,
880
+ // and the two paths do not build `meta` the same way — the fresh
881
+ // path spreads an object, the convergence path decodes through
882
+ // `Schema.decodeUnknownEffect(AttemptMeta)`, which emits keys in
883
+ // schema declaration order. Today the two orders coincide, so the
884
+ // break was latent; adding one optional field to `AttemptMeta` or
885
+ // `BoundaryEvidence` above an existing one, or reordering the spread
886
+ // at line 1405, would have made the convergence re-record compute a
887
+ // different `generation`, append fresh provenance on every later
888
+ // dispatch, and reopen the unbounded-append regression issue #124
889
+ // closed. Canonical JSON makes the stability structural.
890
+ const generation = yield* Schema.decodeUnknownEffect(DerivedKey)({
891
+ kind: "cache-generation",
892
+ meta,
893
+ result: options.result
894
+ }).pipe(Effect.orDie);
895
+ // The provenance record and the row it describes commit together:
896
+ // the row carries the record's canonical seq as its provenance, so a
897
+ // crash between them left either a row pointing at a sequence that
898
+ // does not exist or a `recorded` entry for a row nobody wrote.
899
+ // Losing the first-writer race is not that crash — it is a decision,
900
+ // and it stays journalled: the `recorded` entry says what this run
901
+ // tried to record and the `cache-conflict` entry below says how it
902
+ // resolved.
903
+ const recording = yield* atomically(Effect.gen(function* () {
904
+ const receipt = yield* emitLifecycle(JournalRecords.cacheProvenance({
905
+ runId: deps.runId,
906
+ lineageId,
907
+ cacheKey: keyDigest,
908
+ sourceId: `${deps.sourceId}:cache:${keyDigest}:recorded:${generation}`,
909
+ sourceSeq: 0
910
+ }, { keyDigest, action: "recorded" }));
911
+ const entry = {
912
+ keyDigest,
913
+ result: options.result,
914
+ meta,
915
+ createdAtMs: options.createdAtMs,
916
+ recordedRunId: deps.runId,
917
+ recordedEventSeq: receipt.seq
918
+ };
919
+ const outcome = yield* cache.put(entry);
920
+ // Only an inserting put creates a head, so the insertion is
921
+ // journalled in the same transaction. `StepCacheFold` rebuilds the
922
+ // heads from these entries and never from `recorded`, which a
923
+ // duplicate or a losing conflict also writes (issue #2053).
924
+ if (outcome._tag === "Inserted") {
925
+ const recorded = { runId: deps.runId, eventSeq: receipt.seq };
926
+ yield* emitLifecycle(JournalRecords.cacheProvenance(cacheSource("admitted", recorded), {
927
+ keyDigest,
928
+ action: "admitted",
929
+ recordedRunId: recorded.runId,
930
+ recordedEventSeq: recorded.eventSeq
931
+ }));
932
+ }
933
+ return { entry, outcome };
934
+ })).pipe(Effect.catchTag("@smthrs/step-cache/CacheStoreError", (error) =>
935
+ // Only availability of the local transaction is optional. A known
936
+ // deterministic Conflict below still belongs to Inconsistency.
937
+ noteUnshareable({ stage: "entry", message: `local cache publication failed: ${error.message}` }).pipe(Effect.as(undefined))));
938
+ if (recording === undefined) {
939
+ return;
940
+ }
941
+ // ENTRY LAST, AND OUTSIDE THE TRANSACTION. The local row and its
942
+ // provenance record are now durable together; only here does the
943
+ // entry become observable to other machines, which is the second half
944
+ // of the REAPI ordering constraint. It is deliberately not inside the
945
+ // transaction above: `CacheSync` speaks HTTP, and nothing that is not
946
+ // storage work may be held across a `DurableWriter` write — a stalled
947
+ // shared cache would block every other writer and roll back a row
948
+ // that has nothing to do with it. A `Conflict` skips publication for
949
+ // the reason the local tier reported it: this machine does not agree
950
+ // with itself about the key yet.
951
+ if (unshareable === undefined && recording.outcome._tag !== "Conflict") {
952
+ unshareable = yield* CachePublication.publishEntry(recording.entry);
953
+ }
954
+ if (unshareable !== undefined) {
955
+ // The refusal is journalled so a missing shared entry is
956
+ // explainable, never inferred from its absence. The identity folds
957
+ // a digest of the reason for the same reason the `recorded` record
958
+ // above folds one of its content (issue #129): a convergence
959
+ // re-record hitting the identical refusal is an exact producer
960
+ // retry the journal collapses into a `Duplicate`, while a
961
+ // *different* refusal is a genuinely new observation that journals
962
+ // fresh — and neither can ever be the same identity carrying
963
+ // different content, which is what an idempotency conflict is.
964
+ yield* noteUnshareable(unshareable);
965
+ }
966
+ if (recording.outcome._tag === "Conflict") {
967
+ const conflicting = yield* cache.get(keyDigest);
968
+ if (meta.nondeterministic === true) {
969
+ const recorded = Option.map(conflicting, (entry) => ({
970
+ runId: entry.recordedRunId,
971
+ eventSeq: entry.recordedEventSeq
972
+ }));
973
+ // Declared output nondeterminism is not a hermeticity violation,
974
+ // so it does not enter the Inconsistency receiver. The key folds
975
+ // the declaration, making first-writer-wins safe for both sides.
976
+ yield* emitLifecycle(JournalRecords.cacheProvenance(cacheSource("conflict_first_writer", Option.getOrUndefined(recorded)), {
977
+ keyDigest,
978
+ action: "conflict_first_writer",
979
+ recordedRunId: Option.getOrNull(Option.map(recorded, (value) => value.runId)),
980
+ recordedEventSeq: Option.getOrNull(Option.map(recorded, (value) => value.eventSeq))
981
+ }));
982
+ return;
983
+ }
984
+ const receiverOption = yield* Effect.serviceOption(Inconsistency.Inconsistency);
985
+ // Core default is STRICT: journal the conflict and fail the run,
986
+ // which is Skyframe's throwing `GraphInconsistencyReceiver`.
987
+ // Providing `Inconsistency.layerTolerant` opts out.
988
+ const receiver = Option.isSome(receiverOption)
989
+ ? receiverOption.value
990
+ : Inconsistency.make({ journal, verdict: "fail", owner: deps.owner });
991
+ const verdict = yield* receiver.note({
992
+ key: keyDigest,
993
+ existing: Option.getOrUndefined(conflicting),
994
+ attempted: recording.entry
995
+ });
996
+ if (verdict === "fail") {
997
+ return yield* Effect.fail(new CacheConflictDetected({
998
+ code: "cache_conflict_detected",
999
+ keyDigest,
1000
+ recordedRunId: Option.isSome(conflicting) ? conflicting.value.recordedRunId : "unknown"
1001
+ }));
1002
+ }
1003
+ }
1004
+ });
1005
+ // Everything from the cache-hit verification to the terminal transition
1006
+ // runs under this process's exclusive permit for the attempt key
1007
+ // (issues #102, #103, #118): the adoption decision below is taken from
1008
+ // a read no in-process racer can invalidate before the claim lands, a
1009
+ // concurrent same-key dispatch waits here until the winner's terminal
1010
+ // row is visible and replays it instead of re-executing the body, and
1011
+ // the cache block's read-verify-materialize-evict span can never
1012
+ // interleave with another dispatch's execution. A verified hit returns
1013
+ // from inside the permit. The permit is keyed by (runId, stepKeyDigest)
1014
+ // alone (issue #133): the cache row is addressed with no attempt
1015
+ // material, so folding the attempt counter in let sanctioned keyed
1016
+ // dispatches at skewed attempt counters (#111/#116) acquire different
1017
+ // permits and interleave the very span the permit serializes. The step
1018
+ // key, not the narrowed cache address, is what serializes here, because
1019
+ // the attempt rows two scopes of one key share are what they race over.
1020
+ return yield* admission.withPermit(`${deps.runId}|${stepKeyDigest}`)(Effect.gen(function* () {
1021
+ // Lifecycle announcements take a per-attempt producer identity
1022
+ // (issue #91): adoption — or a replay after a crash in the
1023
+ // finish→emit window (issue #109) — re-emits records a dead
1024
+ // incarnation may already have announced, and lifecycle records
1025
+ // without a `sourceSeq` allocate a fresh journal row on every
1026
+ // emission. A dedicated `(sourceId, sourceSeq 0)` per record makes
1027
+ // the re-emission an exact producer retry the journal collapses
1028
+ // into a `Duplicate`.
1029
+ const attemptSource = (record) => ({
1030
+ runId: deps.runId,
1031
+ lineageId,
1032
+ // A sealed dispatch's result lives in the step cache, so the record
1033
+ // carries the digest that addresses it: replay hands the projection
1034
+ // the sealed value instead of re-deriving it, and a cache miss is
1035
+ // simply an absent value rather than a broken fold.
1036
+ ...(input.tier === "sealed" ? { cacheKey: keyDigest } : {}),
1037
+ sourceId: `${deps.sourceId}:attempt:${stepKeyDigest}:${input.attempt}:${record}`,
1038
+ sourceSeq: 0
1039
+ });
1040
+ /**
1041
+ * Journal-convergence emit for the replay branches (issue #109):
1042
+ * an identical re-emission collapses into a `Duplicate`, and an
1043
+ * `idempotency_conflict` may mean the journal already holds this
1044
+ * record under this producer identity as another lineage recorded
1045
+ * it — a time-travel fork copies the parent's journal rows, so the
1046
+ * copied record names the parent run. A cache-provenance record
1047
+ * may also restate a fact this run already journalled with fresh
1048
+ * measurements (an expiry re-measured after a resume). Those are
1049
+ * the only conflicts this emit tolerates, and only after reading
1050
+ * the occupying record and validating it: an attempt-scoped record
1051
+ * must be this record as a retained fork ancestor emitted it — same
1052
+ * event type, attempt coordinates and terminal state
1053
+ * (`CopiedRecord.accept`); a provenance record must state the same
1054
+ * fact about the same row (`ProvenanceSlot.accept`). An unrelated
1055
+ * or contradictory record in the slot — corrupt or imported
1056
+ * history, a producer collision — surfaces the journal's own
1057
+ * conflict instead of a replay that reports no inconsistency.
1058
+ */
1059
+ const emitConverging = (record) => emitLifecycle(record).pipe(Effect.catch((error) => error.code !== "idempotency_conflict"
1060
+ ? Effect.fail(error)
1061
+ : record.eventType === "flows.engine.cache-provenance"
1062
+ ? ProvenanceSlot.accept({ journal, runId: deps.runId, record, conflict: error })
1063
+ : CopiedRecord.accept({ journal, runs, runId: deps.runId, record, conflict: error })));
1064
+ /**
1065
+ * Records and clears a row the declared time to live has aged out.
1066
+ *
1067
+ * The age judgement is journalled BEFORE the row is dropped, and
1068
+ * with the age and the bound in the payload, so a reader can tell an
1069
+ * expiry from a plain miss and a replay reads the recorded verdict
1070
+ * instead of re-judging the age against a fresh clock. The eviction
1071
+ * is what keeps `CacheStore.put`'s insert-or-nothing head from
1072
+ * turning the re-execution's genuinely newer result into a
1073
+ * `Conflict` — the same repair the stale-read-set branch performs
1074
+ * (issue #99) — and it is fenced on the expired row's own provenance
1075
+ * (issue #119) so a fresh row another process landed in the meantime
1076
+ * survives.
1077
+ */
1078
+ const expireRow = (ttlMs, stale) => Effect.gen(function* () {
1079
+ const recorded = {
1080
+ runId: stale.recordedRunId,
1081
+ eventSeq: stale.recordedEventSeq
1082
+ };
1083
+ const nowMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor));
1084
+ yield* emitConverging(JournalRecords.cacheProvenance(cacheSource("expired", recorded), {
1085
+ keyDigest,
1086
+ action: "expired",
1087
+ ttlMs,
1088
+ ageMs: nowMs - stale.createdAtMs,
1089
+ recordedRunId: recorded.runId,
1090
+ recordedEventSeq: recorded.eventSeq
1091
+ }));
1092
+ yield* cache.evict(keyDigest, { ifRecordedBy: recorded });
1093
+ });
1094
+ /**
1095
+ * Decides — once, durably — whether a row is inside the declared time
1096
+ * to live, and answers the DECISION THIS RUN RECORDED rather than the
1097
+ * one this clock reading would make.
1098
+ *
1099
+ * A time to live is the one cache input that changes answer on its
1100
+ * own. A dispatch that served a row at 900 ms and then lost its
1101
+ * process would, on the plain reading, re-dispatch at 1100 ms, expire
1102
+ * the row it already served, and execute a body whose result the run
1103
+ * had already consumed. The verdict is therefore journalled before it
1104
+ * is acted on, under a producer identity that names the step key, the
1105
+ * `ttl` decision, and the row's own recorded provenance
1106
+ * (`cacheSource`), with `sourceSeq` 0. That identity belongs to the
1107
+ * RUN, not to the incarnation driving it, so the engine that resumes
1108
+ * the run reads the same verdict rather than taking a second one.
1109
+ *
1110
+ * `Accepted` records the first decision; `Duplicate` confirms it.
1111
+ * A conflict alone does not identify the recorded verdict: TTL is
1112
+ * not part of every action key, and copied history can carry other
1113
+ * lineage metadata. Only an exact duplicate of the opposite verdict
1114
+ * proves a clock change. Otherwise fail before serving or expiring
1115
+ * the row, preserving the incompatible history for the caller.
1116
+ *
1117
+ * A row re-recorded under a provenance this run already expired keeps
1118
+ * that verdict, which is the same rule read from the other side.
1119
+ */
1120
+ const admitByAge = (ttlMs, row) => Effect.gen(function* () {
1121
+ const recorded = { runId: row.recordedRunId, eventSeq: row.recordedEventSeq };
1122
+ const nowMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor));
1123
+ const measured = nowMs - row.createdAtMs <= ttlMs ? "admitted" : "expired";
1124
+ const decision = (verdict) => JournalRecords.cacheProvenance(cacheSource("ttl", recorded), {
1125
+ keyDigest,
1126
+ action: "ttl",
1127
+ ttlMs,
1128
+ verdict,
1129
+ recordedRunId: recorded.runId,
1130
+ recordedEventSeq: recorded.eventSeq
1131
+ });
1132
+ // A copied producer whose source fields changed must not look
1133
+ // like a new first decision merely because emit would accept it.
1134
+ const expectedSource = cacheSource("ttl", recorded).sourceId;
1135
+ const prior = yield* CacheAgeHistory.find(journal, deps.runId, (entry) => {
1136
+ const payload = entry.payload;
1137
+ return entry.sourceId === expectedSource ||
1138
+ (payload?.action === "ttl" && payload.keyDigest === keyDigest &&
1139
+ payload.recordedRunId === recorded.runId && payload.recordedEventSeq === recorded.eventSeq);
1140
+ });
1141
+ if (prior !== undefined && (prior.sourceId !== expectedSource || prior.sourceSeq !== 0)) {
1142
+ yield* emitConverging(JournalRecords.cacheProvenance(cacheSource("replay_failed", recorded), {
1143
+ keyDigest,
1144
+ action: "replay_failed",
1145
+ reason: "incompatible-age-history",
1146
+ recordedRunId: recorded.runId,
1147
+ recordedEventSeq: recorded.eventSeq
1148
+ }));
1149
+ return yield* Effect.fail(new Journal.JournalError({
1150
+ code: "idempotency_conflict",
1151
+ message: "incompatible recorded cache-age decision: producer identity changed",
1152
+ cause: prior
1153
+ }));
1154
+ }
1155
+ return yield* emitLifecycle(decision(measured)).pipe(Effect.as(measured), Effect.catch((error) => {
1156
+ if (error.code !== "idempotency_conflict") {
1157
+ return Effect.fail(error);
1158
+ }
1159
+ const conflict = new Journal.JournalError({
1160
+ code: "idempotency_conflict",
1161
+ message: "incompatible recorded cache-age decision: restore the original TTL and history identity " +
1162
+ "or use a new action identity; the cache row was not served or expired",
1163
+ cause: error
1164
+ });
1165
+ const opposite = measured === "admitted" ? "expired" : "admitted";
1166
+ return emitLifecycle(decision(opposite)).pipe(Effect.catch((oppositeError) => oppositeError.code === "idempotency_conflict"
1167
+ ? CacheAgeHistory.copiedVerdict({
1168
+ journal,
1169
+ runs,
1170
+ runId: deps.runId,
1171
+ decision: decision(measured),
1172
+ conflict
1173
+ }).pipe(Effect.tapError((failure) => failure.code === "idempotency_conflict"
1174
+ ? emitConverging(JournalRecords.cacheProvenance(cacheSource("replay_failed", recorded), {
1175
+ keyDigest,
1176
+ action: "replay_failed",
1177
+ reason: "incompatible-age-history",
1178
+ recordedRunId: recorded.runId,
1179
+ recordedEventSeq: recorded.eventSeq
1180
+ })).pipe(Effect.asVoid)
1181
+ : Effect.void), Effect.map((verdict) => ({ _tag: "Historical", verdict })))
1182
+ : Effect.fail(oppositeError)), Effect.flatMap((receipt) => receipt._tag === "Historical" ?
1183
+ Effect.succeed(receipt.verdict) :
1184
+ receipt._tag === "Duplicate"
1185
+ ? Effect.succeed(opposite)
1186
+ : Effect.fail(conflict)));
1187
+ }));
1188
+ });
1189
+ /**
1190
+ * The row this dispatch may serve, after the declared age bound.
1191
+ *
1192
+ * The bound is applied here rather than inside `cache.get` so the
1193
+ * refusal is a decision this run journalled and owns, not a read
1194
+ * policy the store re-derives from a fresh clock on every lookup.
1195
+ * The no-TTL guard refreshes a per-executor verdict index through
1196
+ * the journal's current tail before answering absence. Its cursor
1197
+ * covers only fully processed pages in the same journal generation;
1198
+ * resume or a changed history rebuilds it once, never per dispatch.
1199
+ */
1200
+ const admissible = (ttlMs, row) => Effect.gen(function* () {
1201
+ // Removing TTL cannot bypass a verdict this run already consumed,
1202
+ // even if the head was subsequently evicted or replaced.
1203
+ if (ttlMs === undefined) {
1204
+ const recorded = yield* recordedAgeVerdict(journal, keyDigest);
1205
+ if (recorded !== undefined) {
1206
+ return yield* Effect.fail(new Journal.JournalError({
1207
+ code: "idempotency_conflict",
1208
+ message: "incompatible recorded cache-age decision: ttlMs cannot be removed after a recorded verdict",
1209
+ cause: recorded
1210
+ }));
1211
+ }
1212
+ return row;
1213
+ }
1214
+ if (Option.isNone(row)) {
1215
+ return row;
1216
+ }
1217
+ if ((yield* admitByAge(ttlMs, row.value)) === "admitted") {
1218
+ return row;
1219
+ }
1220
+ yield* expireRow(ttlMs, row.value);
1221
+ return Option.none();
1222
+ });
1223
+ // A run's admitted attempt is replay authority. Shared cache rows
1224
+ // are replaceable and cannot overwrite its result or failure.
1225
+ const existing = yield* attempts.get(attemptId);
1226
+ if (cacheDeclaration._tag === "Eligible") {
1227
+ const observed = yield* cache.get(keyDigest).pipe(Effect.catch((error) => noteUnshareable({ stage: "entry", message: `cache lookup failed: ${error.message}` }).pipe(Effect.as(Option.none()))));
1228
+ const evidenceDecision = CacheAdmission.candidate(Option.isSome(observed) ? decodeMeta(observed.value.meta) : undefined);
1229
+ const outputDecision = evidenceDecision._tag === "CandidateEvidence"
1230
+ ? CacheOutputPolicy.replay(input.metadata, evidenceDecision.evidence)
1231
+ : undefined;
1232
+ const candidate = outputDecision?._tag === "Refused" ? outputDecision : evidenceDecision;
1233
+ // A descriptor refusal cannot be turned into TTL eviction. Validate
1234
+ // replay authority before any age decision is allowed to prune a head.
1235
+ const cached = Option.isNone(observed) || candidate._tag === "CandidateEvidence" || policy?.ttlMs === undefined
1236
+ ? yield* admissible(policy?.ttlMs, observed)
1237
+ : observed;
1238
+ if (Option.isSome(cached)) {
1239
+ if (candidate._tag === "Refused") {
1240
+ yield* emitConverging(JournalRecords.cacheProvenance(cacheSource("replay_failed", {
1241
+ runId: cached.value.recordedRunId,
1242
+ eventSeq: cached.value.recordedEventSeq
1243
+ }), {
1244
+ keyDigest,
1245
+ action: "replay_failed",
1246
+ reason: candidate.reason,
1247
+ recordedRunId: cached.value.recordedRunId,
1248
+ recordedEventSeq: cached.value.recordedEventSeq
1249
+ }));
1250
+ }
1251
+ if (candidate._tag === "CandidateEvidence" && Option.isNone(existing)) {
1252
+ const boundary = yield* StepBoundary.StepBoundary;
1253
+ // Skyframe's dirty check, not "the declaration changed" (issue
1254
+ // #90): the read-set digests folded into the step key are caller
1255
+ // metadata, so reuse is justified only once the host has
1256
+ // measured them and agreed. A stale declaration falls through to
1257
+ // a real execution instead of replaying a pre-edit result, and
1258
+ // the refusal is journalled so it is visible rather than silent.
1259
+ // A boundary the host cannot enforce is likewise not a hit; the
1260
+ // dispatch path below re-prepares and fails the attempt properly.
1261
+ const measured = yield* boundary.prepare(cacheDeclaration.metadata).pipe(Effect.option);
1262
+ const verified = Option.isSome(measured) && StepBoundary.readSetMatches(measured.value);
1263
+ if (verified) {
1264
+ const evidence = candidate.evidence;
1265
+ // Cache materialization is host work too. The authorized
1266
+ // invocation is durable before it, even without a new attempt.
1267
+ yield* emitCallInvoked;
1268
+ let materialized = yield* boundary.replayOutputs(evidence).pipe(Effect.exit);
1269
+ if (Exit.isFailure(materialized) &&
1270
+ CachePublication.replayMissingArtifact(materialized.cause) !== undefined) {
1271
+ // LAZY DOWNLOAD (issue #172). A shared cache row is
1272
+ // routinely recorded on a machine whose artifacts this one
1273
+ // has never seen, so "the blob is not here" is the normal
1274
+ // first answer, not a defect — and it is the one replay
1275
+ // refusal a shared artifact tier can repair. Fetch, verify,
1276
+ // write back, and retry the replay ONCE. Only once: a
1277
+ // second failure means the tier cannot serve it either, and
1278
+ // the fall-through below (a real execution) is strictly
1279
+ // better than looping. With no shared tier configured
1280
+ // `hydrateArtifacts` reports `false` and this costs one
1281
+ // cheap branch.
1282
+ if (yield* CachePublication.hydrateArtifacts(evidence)) {
1283
+ materialized = yield* boundary.replayOutputs(evidence).pipe(Effect.exit);
1284
+ }
1285
+ }
1286
+ if (Exit.isSuccess(materialized)) {
1287
+ const recorded = {
1288
+ runId: cached.value.recordedRunId,
1289
+ eventSeq: cached.value.recordedEventSeq
1290
+ };
1291
+ yield* emitConverging(JournalRecords.cacheProvenance(cacheSource("hit", recorded), {
1292
+ keyDigest,
1293
+ recordedRunId: cached.value.recordedRunId,
1294
+ recordedEventSeq: cached.value.recordedEventSeq
1295
+ }));
1296
+ // Counted after the provenance emit: `verified_hit` means
1297
+ // the cached result was served, and a journal failure on
1298
+ // the emit fails the dispatch before any result is
1299
+ // returned. A dispatch that dies mid-decision records no
1300
+ // decision; its exit lands in `flows_engine_dispatches`.
1301
+ yield* Metric.update(EngineStoreMetrics.stepCacheDecision.VerifiedHit, 1);
1302
+ // Served from a record: the body never ran, and the node
1303
+ // above this dispatch is told so rather than left to
1304
+ // report work nobody did.
1305
+ yield* Action.reportDispatch({
1306
+ outcome: "replayed",
1307
+ stepKeyDigest,
1308
+ attempt: input.attempt
1309
+ });
1310
+ return cached.value.result;
1311
+ }
1312
+ // Evidence the host cannot re-materialize — a transient
1313
+ // filesystem error, or a row recorded by a foreign boundary
1314
+ // implementation — is not a hit and not a run failure
1315
+ // (issue #107): failing here while the verified row survived
1316
+ // repeated refuse→fail on every later run, the exact
1317
+ // permanent-failure loop #99 closed one branch later. The
1318
+ // refusal is journalled and the dispatch path below executes
1319
+ // for real; the row survives for hosts that can replay it.
1320
+ // The refusal record carries its classification (issue
1321
+ // #150): `corruption` for a digest mismatch at the
1322
+ // content-addressed blob path, `host` for everything else —
1323
+ // a failing disk corrupting many blobs must never journal
1324
+ // identically to a one-off EIO.
1325
+ const corruption = replayCorruption(materialized.cause);
1326
+ yield* emitConverging(JournalRecords.cacheProvenance(cacheSource("replay_failed", {
1327
+ runId: cached.value.recordedRunId,
1328
+ eventSeq: cached.value.recordedEventSeq
1329
+ }), {
1330
+ keyDigest,
1331
+ action: "replay_failed",
1332
+ reason: corruption === undefined ? "host" : "corruption",
1333
+ recordedRunId: cached.value.recordedRunId,
1334
+ recordedEventSeq: cached.value.recordedEventSeq
1335
+ }));
1336
+ if (corruption !== undefined) {
1337
+ // Corruption is an integrity violation, not a retryable
1338
+ // refusal: it routes to the Inconsistency receiver like
1339
+ // a cache-key conflict does. The core default is STRICT
1340
+ // (fail); `Inconsistency.layerTolerant` (or a plugin
1341
+ // verdict) lets the dispatch fall back to the real
1342
+ // execution below, whose re-capture heals the address.
1343
+ const receiverOption = yield* Effect.serviceOption(Inconsistency.Inconsistency);
1344
+ const receiver = Option.isSome(receiverOption)
1345
+ ? receiverOption.value
1346
+ : Inconsistency.make({ journal, verdict: "fail", owner: deps.owner });
1347
+ const verdict = yield* receiver.noteCorruption({
1348
+ runId: deps.runId,
1349
+ keyDigest,
1350
+ path: corruption.path,
1351
+ recordedDigest: corruption.recordedDigest,
1352
+ measuredDigest: corruption.measuredDigest,
1353
+ recordedRunId: cached.value.recordedRunId,
1354
+ recordedEventSeq: cached.value.recordedEventSeq
1355
+ });
1356
+ // Quarantine is journal AND evict (issue #164). The
1357
+ // receiver's durable record preserved the evidence, but
1358
+ // leaving the row in place made the poison permanent for
1359
+ // INLINE evidence: `CacheStore.put` is insert-or-nothing,
1360
+ // so a tolerant re-execution never replaced the corrupt
1361
+ // bytes and re-detected them on every later run, while
1362
+ // strict mode re-failed the key forever. Evicting under
1363
+ // both verdicts lets the next dispatch — the tolerant
1364
+ // fall-through below, or the run after a strict failure —
1365
+ // execute and record cleanly. The evict is fenced on the
1366
+ // poisoned row's own provenance like the stale-read-set
1367
+ // branch (issue #119): a fresh row landed by a concurrent
1368
+ // run between this dispatch's `get` and the `evict` makes
1369
+ // the compare-and-swap a no-op instead of deleting valid
1370
+ // evidence.
1371
+ yield* cache.evict(keyDigest, {
1372
+ ifRecordedBy: {
1373
+ runId: cached.value.recordedRunId,
1374
+ eventSeq: cached.value.recordedEventSeq
1375
+ }
1376
+ });
1377
+ if (verdict === "fail") {
1378
+ return yield* Effect.fail(new CacheCorruptionDetected({
1379
+ code: "cache_corruption_detected",
1380
+ keyDigest,
1381
+ path: corruption.path,
1382
+ recordedDigest: corruption.recordedDigest,
1383
+ measuredDigest: corruption.measuredDigest
1384
+ }));
1385
+ }
1386
+ }
1387
+ // `replay_failed` is a fall-through decision, so it is
1388
+ // counted only once the refusal emit landed and the strict
1389
+ // corruption verdict above has NOT terminated the dispatch:
1390
+ // a dispatch that fails instead of re-executing records no
1391
+ // decision.
1392
+ yield* Metric.update(EngineStoreMetrics.stepCacheDecision.ReplayFailed, 1);
1393
+ }
1394
+ else if (Option.isSome(measured)) {
1395
+ // Only a *measured* mismatch is evidence the inputs changed
1396
+ // (issue #110): a host that cannot measure right now — a
1397
+ // transient EIO/EACCES on any declared read path — says
1398
+ // nothing about the read set, so the hit is merely refused for
1399
+ // this dispatch (the path below re-prepares and surfaces the
1400
+ // host failure as an ordinary attempt failure) and the valid
1401
+ // shared row survives for every run whose host is healthy.
1402
+ //
1403
+ // The durable emit is also the fence: `emitDurable` fails with
1404
+ // `fence_lost` for a zombie that lost the run, surfacing as
1405
+ // self-interruption before the eviction below can run.
1406
+ yield* emitConverging(JournalRecords.cacheProvenance(cacheSource("stale_read_set", {
1407
+ runId: cached.value.recordedRunId,
1408
+ eventSeq: cached.value.recordedEventSeq
1409
+ }), {
1410
+ keyDigest,
1411
+ action: "stale_read_set",
1412
+ recordedRunId: cached.value.recordedRunId,
1413
+ recordedEventSeq: cached.value.recordedEventSeq
1414
+ }));
1415
+ // Skyframe invalidation, not just refusal (issue #99): a stale
1416
+ // read set means the inputs changed, so the re-execution's
1417
+ // result is *expected* to differ — left in place, the poisoned
1418
+ // row turns the fresh `cache.put` into a Conflict, the strict
1419
+ // verdict fails the run, and nothing ever removes the row, so
1420
+ // every later run repeats the refuse → re-execute → conflict →
1421
+ // fail cycle. The refusal is journalled above; evicting here
1422
+ // lets the re-execution record cleanly under the same key.
1423
+ // The eviction is fenced on the poisoned row's own
1424
+ // provenance (issue #119): a fresh entry recorded by a
1425
+ // concurrent run between this dispatch's `get` and its
1426
+ // `evict` must not be deleted with the poison (issue #110).
1427
+ // The permit (issue #118) only closes the *in-process*
1428
+ // window, so the guard rides inside the DELETE rather than
1429
+ // in a preceding read — a foreign process landing a fresh
1430
+ // row simply makes the compare-and-swap a no-op.
1431
+ yield* cache.evict(keyDigest, {
1432
+ ifRecordedBy: {
1433
+ runId: cached.value.recordedRunId,
1434
+ eventSeq: cached.value.recordedEventSeq
1435
+ }
1436
+ });
1437
+ // The fall-through decision is counted once the refusal is
1438
+ // durable and the poisoned row is gone: a fenced-out zombie
1439
+ // self-interrupts at the emit above and records no decision.
1440
+ yield* Metric.update(EngineStoreMetrics.stepCacheDecision.StaleReadSet, 1);
1441
+ }
1442
+ else {
1443
+ // The host could not measure the read set at all, so the
1444
+ // hit is merely refused for this dispatch; the row survives.
1445
+ yield* Metric.update(EngineStoreMetrics.stepCacheDecision.Unmeasurable, 1);
1446
+ }
1447
+ }
1448
+ else if (candidate._tag === "Refused") {
1449
+ // A row whose recorded evidence cannot justify reuse — a
1450
+ // foreign tier, an unverified capture, a recorded deviation.
1451
+ yield* Metric.update(EngineStoreMetrics.stepCacheDecision.UnverifiableEvidence, 1);
1452
+ }
1453
+ }
1454
+ else {
1455
+ yield* Metric.update(EngineStoreMetrics.stepCacheDecision.Miss, 1);
1456
+ }
1457
+ }
1458
+ if (Option.isSome(existing)) {
1459
+ const row = existing.value;
1460
+ if (row.state === "succeeded") {
1461
+ yield* emitCallInvoked;
1462
+ const meta = decodeMeta(row.meta);
1463
+ let corruptEvidence = false;
1464
+ const outputDecision = meta?.boundary === undefined
1465
+ ? undefined
1466
+ : CacheOutputPolicy.replay(input.metadata, meta.boundary);
1467
+ const storedDecision = CacheAdmission.candidate(meta);
1468
+ const refused = storedDecision._tag === "Refused" &&
1469
+ (storedDecision.reason === "contradictory-evidence" ||
1470
+ storedDecision.reason === "quarantined-evidence")
1471
+ ? storedDecision
1472
+ : outputDecision;
1473
+ if (refused?._tag === "Refused") {
1474
+ yield* emitConverging(JournalRecords.cacheProvenance(cacheSource("replay_failed"), {
1475
+ keyDigest,
1476
+ action: "replay_failed",
1477
+ reason: refused.reason
1478
+ }));
1479
+ }
1480
+ if (meta?.boundary !== undefined && refused?._tag !== "Refused") {
1481
+ const boundary = yield* StepBoundary.StepBoundary;
1482
+ const materialized = yield* boundary.replayOutputs(meta.boundary).pipe(Effect.exit);
1483
+ if (Exit.isFailure(materialized)) {
1484
+ // The attempt durably succeeded: its recorded outcome is
1485
+ // the truth, and re-materializing the workspace outputs is
1486
+ // best-effort — failing the dispatch here while the
1487
+ // terminal row survived recreated the #99 permanent
1488
+ // refuse→fail loop one branch earlier (issue #107). The
1489
+ // refusal is journalled so a missing output is
1490
+ // explainable rather than silent, and it carries its
1491
+ // classification (issue #150): corruption of the recorded
1492
+ // evidence routes to the Inconsistency receiver instead of
1493
+ // passing as an ordinary transient host refusal.
1494
+ const corruption = replayCorruption(materialized.cause);
1495
+ corruptEvidence = corruption !== undefined;
1496
+ yield* emitConverging(JournalRecords.cacheProvenance(cacheSource("replay_failed"), {
1497
+ keyDigest,
1498
+ action: "replay_failed",
1499
+ reason: corruption === undefined ? "host" : "corruption"
1500
+ }));
1501
+ if (corruption !== undefined) {
1502
+ const receiverOption = yield* Effect.serviceOption(Inconsistency.Inconsistency);
1503
+ const receiver = Option.isSome(receiverOption)
1504
+ ? receiverOption.value
1505
+ : Inconsistency.make({ journal, verdict: "fail", owner: deps.owner });
1506
+ const verdict = yield* receiver.noteCorruption({
1507
+ runId: deps.runId,
1508
+ // The attempt row, not a cache row: this evidence is
1509
+ // addressed by (runId, stepKeyDigest, attempt).
1510
+ keyDigest: stepKeyDigest,
1511
+ path: corruption.path,
1512
+ recordedDigest: corruption.recordedDigest,
1513
+ measuredDigest: corruption.measuredDigest
1514
+ });
1515
+ // Quarantine is journal AND take a state action (issues
1516
+ // #164, #171). Unlike a shared cache row, this succeeded
1517
+ // attempt cannot be evicted: its side effects already ran,
1518
+ // so a miss would re-execute potentially irreversible work.
1519
+ // Remove only the corrupt boundary evidence and mark the
1520
+ // row quarantined. The durable outcome stays intact and the
1521
+ // next dispatch returns it without materializing the poison
1522
+ // or publishing the row back into the shared cache.
1523
+ //
1524
+ // The owner heartbeat and patch share one write
1525
+ // transaction. The patch is owner-fenced itself — it only
1526
+ // lands while `flows_runs` still records `deps.owner` —
1527
+ // and the heartbeat both refreshes the lease and reports
1528
+ // the loss as a run-store outcome before the patch runs.
1529
+ // `boundary` is dropped by omitting the key, never by
1530
+ // writing `undefined` over it: the attempt store admits
1531
+ // inert JSON, and an own property holding `undefined` is
1532
+ // not JSON, so the patch would be refused outright.
1533
+ // New quarantine writes clear both reusable proof fields;
1534
+ // contradictory legacy rows remain readable without repair.
1535
+ const { boundary: _quarantined, readSetVerified: _unverified, ...retainedMeta } = meta;
1536
+ const quarantinedMeta = {
1537
+ ...retainedMeta,
1538
+ boundaryQuarantined: true
1539
+ };
1540
+ const quarantined = yield* atomically(Effect.gen(function* () {
1541
+ const quarantineAtMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor));
1542
+ const fence = yield* runs.heartbeat(deps.runId, deps.owner, quarantineAtMs);
1543
+ if (fence._tag !== "Updated") {
1544
+ return false;
1545
+ }
1546
+ const patched = yield* attempts.patch(attemptId, { meta: attemptPayload(quarantinedMeta) }, deps.owner);
1547
+ return patched._tag === "Patched";
1548
+ }));
1549
+ if (!quarantined) {
1550
+ return yield* Effect.interrupt;
1551
+ }
1552
+ if (verdict === "fail") {
1553
+ // The strict verdict still makes the integrity violation
1554
+ // visible by parking this dispatch. The row repair above
1555
+ // is what makes the park resumable without an out-of-band
1556
+ // byte repair. This is a defect, not a declared action
1557
+ // business error: routing it through the failure channel
1558
+ // would make the action's error schema replace it with
1559
+ // a SchemaError.
1560
+ return yield* Effect.die(new AttemptEvidenceQuarantined({
1561
+ code: "attempt_evidence_quarantined",
1562
+ keyDigest: stepKeyDigest,
1563
+ attempt: input.attempt,
1564
+ path: corruption.path,
1565
+ recordedDigest: corruption.recordedDigest,
1566
+ measuredDigest: corruption.measuredDigest
1567
+ }));
1568
+ }
1569
+ }
1570
+ }
1571
+ }
1572
+ // Converge the cache with the durable completion: a crash between
1573
+ // `attempts.finish` and `cache.put` otherwise leaves the sealed
1574
+ // result permanently missing from the shared cache (issue #24).
1575
+ // Reaching this branch with an eligible declaration means `cache.get`
1576
+ // above missed or was unfit for replay.
1577
+ // Only a row whose recorded read set was verified at prepare
1578
+ // time may converge into the shared cache (issue #106): an
1579
+ // unverified result was computed against content the key does
1580
+ // not describe.
1581
+ // A row whose boundary evidence was just measured corrupt is
1582
+ // quarantined, never converged (issue #160): a `tolerate`
1583
+ // verdict keeps the durable outcome as this run's truth, but
1584
+ // publishing the known-corrupt evidence into the shared cache
1585
+ // would hand sibling runs a poisoned hit under this run's
1586
+ // provenance.
1587
+ const admission = CacheAdmission.completion(cacheDeclaration, meta, corruptEvidence || refused?._tag === "Refused");
1588
+ if (admission._tag === "PublishCompletion") {
1589
+ yield* recordCache({
1590
+ result: row.outcome,
1591
+ admission,
1592
+ createdAtMs: row.finishedAtMs ?? (yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor)))
1593
+ });
1594
+ }
1595
+ // Converge the journal with the durable completion (issue
1596
+ // #109): a crash between `attempts.finish` and the terminal
1597
+ // emits left `attemptStarted` without `attemptFinished`
1598
+ // forever. The per-attempt producer identity collapses the
1599
+ // re-emission into a `Duplicate` on ordinary replays.
1600
+ if (meta?.boundary?.deviation !== undefined) {
1601
+ yield* emitConverging(JournalRecords.expectedSetDeviation(attemptSource("deviation"), {
1602
+ ...attemptId,
1603
+ ...meta.boundary.deviation
1604
+ }));
1605
+ }
1606
+ // Replays publish the saved first observation, never a newly
1607
+ // computed timestamp or the current history generation.
1608
+ yield* emitStepSettled(row.outcome);
1609
+ yield* emitConverging(JournalRecords.attemptFinished(attemptSource("finished"), { ...attemptId, state: "succeeded" }));
1610
+ // Upgrade an older durable controller record without rerunning
1611
+ // its handler; current records collapse to the original receipt.
1612
+ yield* emitCallSettled(row.outcome);
1613
+ // A durable attempt row answered the dispatch, so nothing ran.
1614
+ yield* Action.reportDispatch({ outcome: "replayed", stepKeyDigest, attempt: input.attempt });
1615
+ return row.outcome;
1616
+ }
1617
+ if (row.state === "failed") {
1618
+ yield* emitCallInvoked;
1619
+ // Converge the journal before rethrowing (issue #109): the
1620
+ // violation kind survives in the row meta because the
1621
+ // persisted cause alone cannot distinguish a boundary
1622
+ // violation from an ordinary execution failure.
1623
+ if (decodeMeta(row.meta)?.hardViolation === true) {
1624
+ yield* emitConverging(JournalRecords.hardViolation(attemptSource("hard-violation"), {
1625
+ ...attemptId,
1626
+ error: rehydrateCause(row.error)
1627
+ }));
1628
+ }
1629
+ yield* emitConverging(JournalRecords.attemptFinished(attemptSource("finished"), { ...attemptId, state: "failed" }));
1630
+ // A durably failed attempt is replayed by rethrowing the persisted
1631
+ // domain failure — never by readmission (issue #59). Falling
1632
+ // through to `attempts.put` here surfaced the row as
1633
+ // `AttemptAdmissionRejected`, whose tag can never match a
1634
+ // policy-declared `nonRetryable` classification, so a durably
1635
+ // failed no-retry action earned an extra real dispatch after
1636
+ // resume. Temporal's prior art: mutable state persists the attempt
1637
+ // failure and `ExecutionInfo.Attempt`, and its no-retry decision
1638
+ // (`service/history/workflow/retry.go`) is re-evaluated from that
1639
+ // persisted failure — the failure itself is durable, not just the
1640
+ // fact that an attempt happened.
1641
+ return yield* Effect.failCause(rehydrateCause(row.error));
1642
+ }
1643
+ if (row.state === "suspended") {
1644
+ return yield* Effect.fail(new AttemptSuspended({
1645
+ code: "attempt_suspended",
1646
+ runId: deps.runId,
1647
+ keyDigest: stepKeyDigest,
1648
+ attempt: input.attempt
1649
+ }));
1650
+ }
1651
+ }
1652
+ /**
1653
+ * A persisted `running` row read while this owner holds the run fence
1654
+ * is crash evidence, not a live admission (issue #71): the incarnation
1655
+ * that admitted the attempt died before finishing (SIGKILL, OOM), and
1656
+ * the #53 stale-running sweep re-drove the run to a new owner — or the
1657
+ * same owner after restart. The attempt never completed, so it must
1658
+ * re-execute under its original number rather than fall through to
1659
+ * `attempts.put` (whose `Conflict` on the differing `startedAtMs`
1660
+ * surfaced as `AttemptAdmissionRejected`, permanently failing a
1661
+ * no-policy run with an infrastructure tag). The row is adopted; the
1662
+ * ordinary fenced `attempts.finish` transition below records the
1663
+ * re-execution's outcome.
1664
+ *
1665
+ * Adoption requires liveness evidence (issue #86), and that evidence is
1666
+ * the admission permit held around this whole span (issues #102, #103):
1667
+ * a live same-key dispatch of this process would be holding the permit,
1668
+ * so a `running` row observed here cannot belong to a live in-process
1669
+ * fiber — it is a dead fiber of this incarnation (an in-process
1670
+ * re-drive after an interrupt, the #71 mode the recorded-nonce guard
1671
+ * wrongly refused) or a superseded incarnation (which provably lost the
1672
+ * run fence this owner holds). Deciding from the recorded `admittedBy`
1673
+ * nonce could not tell those apart, and comparing it against a stale
1674
+ * pre-claim read left a TOCTOU window where two concurrent dispatches
1675
+ * both saw a dead owner and both executed an irreversible body.
1676
+ */
1677
+ const runningRow = Option.isSome(existing) && existing.value.state === "running"
1678
+ ? existing.value
1679
+ : undefined;
1680
+ const runningMeta = runningRow === undefined ? undefined : decodeMeta(runningRow.meta);
1681
+ /**
1682
+ * ADOPTION READS THE CROSSING BACK. A `running` row is crash
1683
+ * evidence, but "the attempt did not settle" is not "the body did
1684
+ * not run": an effect whose terminal boundary is already durable was
1685
+ * still re-dispatched here, because the row alone cannot say so and
1686
+ * nothing ever read the boundary record back. That made a keyless
1687
+ * irreversible action charge twice on a SIGKILL landing in the
1688
+ * millisecond between the `succeeded` boundary and `attempts.finish`
1689
+ * — the exact window the boundary record exists to close.
1690
+ *
1691
+ * A recorded `succeeded` crossing settles the adopted attempt from
1692
+ * the outcome the crossing wrote, in this attempt's own number, and
1693
+ * the body never runs again. The outcome comes from the ROW, not
1694
+ * from the journal record: journal payloads go through redaction on
1695
+ * write, so the record's `output` is observability and the row's
1696
+ * `outcome` is the executable copy (see `AttemptMeta.effectCrossing`).
1697
+ */
1698
+ if (runningRow !== undefined && runningMeta?.effectCrossing === "succeeded") {
1699
+ yield* emitCallInvoked;
1700
+ const finishedAtMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor));
1701
+ // `outcome` and `meta` are deliberately omitted: `FinishAttempt`
1702
+ // leaves an omitted field as recorded, so the terminal transition
1703
+ // is the crossing's own result being sealed rather than a value
1704
+ // this incarnation invented.
1705
+ const finished = yield* settleAttempt({ ...attemptId, state: "succeeded", finishedAtMs }, [JournalRecords.attemptFinished(attemptSource("finished"), { ...attemptId, state: "succeeded" })]);
1706
+ if (!finished) {
1707
+ return yield* Effect.interrupt;
1708
+ }
1709
+ return runningRow.outcome;
1710
+ }
1711
+ /**
1712
+ * AN UNRESOLVED CROSSING IS NOT SILENTLY RE-DISPATCHED. `intended`
1713
+ * without a terminal crossing means the body was dispatched and
1714
+ * nothing durable says how it ended: the charge may have gone
1715
+ * through, and no record can settle the question. An idempotency key
1716
+ * is the one thing that makes the repeat safe, so with a key the
1717
+ * attempt re-executes exactly as before, and without one this
1718
+ * refuses under the same error a numbered retry refuses under. The
1719
+ * run stops with a typed, durable "we cannot tell whether this
1720
+ * happened" for an operator, which is the honest outcome; charging a
1721
+ * second time to keep the run moving is not.
1722
+ *
1723
+ * Only tier 3 refuses. A compensable body is re-executed on a
1724
+ * restored pre-image (issue #87), which is what makes its repeat
1725
+ * safe by construction.
1726
+ */
1727
+ if (input.tier === "irreversible" &&
1728
+ runningMeta?.effectCrossing === "intended" &&
1729
+ deps.idempotencyKey === undefined) {
1730
+ return yield* Effect.fail(new Action.IrreversibleRetryRequiresIdempotencyKey({
1731
+ actionName: actionKind(input.action),
1732
+ attempt: input.attempt
1733
+ }));
1734
+ }
1735
+ // THE REPLAY-ONLY GATE. Every durable record that could serve this
1736
+ // dispatch has been consulted above; what remains is admitting an
1737
+ // attempt and running the body. A replay-only engine stops here,
1738
+ // before the attempt row, the snapshot, the boundary and the body.
1739
+ if (deps.replayOnly !== undefined) {
1740
+ replay.gated = true;
1741
+ const refused = {
1742
+ runId: deps.runId,
1743
+ stepKeyDigest,
1744
+ attempt: input.attempt,
1745
+ action: actionKind(input.action)
1746
+ };
1747
+ yield* deps.replayOnly.observe({
1748
+ ...refused,
1749
+ tier: input.tier,
1750
+ outcome: runningRow === undefined ? "would-execute" : "resumes"
1751
+ });
1752
+ return yield* Effect.die(new ReplayOnly.WouldExecute(refused));
1753
+ }
1754
+ const adopted = runningRow !== undefined;
1755
+ // The admission row and its announcement commit as one unit: an
1756
+ // `attemptStarted` never describes a row that rolled back, and an
1757
+ // admitted attempt is never invisible to the journal.
1758
+ yield* atomically(Effect.gen(function* () {
1759
+ if (!adopted) {
1760
+ const now = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor));
1761
+ const initialMeta = { ...declarationMeta, admittedBy: deps.owner };
1762
+ const put = yield* attempts.put({ ...attemptId, state: "running", startedAtMs: now, meta: attemptPayload(initialMeta) }, deps.owner);
1763
+ if (put._tag === "FenceLost" || put._tag === "RunNotFound") {
1764
+ return yield* Effect.interrupt;
1765
+ }
1766
+ if (put._tag !== "Inserted") {
1767
+ return yield* Effect.fail(new AttemptAdmissionRejected({
1768
+ code: "attempt_admission_rejected",
1769
+ keyDigest: stepKeyDigest,
1770
+ outcome: put._tag
1771
+ }));
1772
+ }
1773
+ }
1774
+ else {
1775
+ // The claim is fenced at the moment it lands (issue #102): re-verify
1776
+ // run ownership immediately before re-homing the row, so a process
1777
+ // that lost the fence while waiting on the permit parks instead of
1778
+ // patching a run it no longer owns. The patch below carries the
1779
+ // owner fence itself; the heartbeat additionally refreshes the
1780
+ // lease, and the permit excludes in-process racers.
1781
+ const claimAtMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor));
1782
+ const claimFence = yield* runs.heartbeat(deps.runId, deps.owner, claimAtMs);
1783
+ if (claimFence._tag !== "Updated") {
1784
+ return yield* Effect.interrupt;
1785
+ }
1786
+ // Re-home the adopted row to the current incarnation; the patch
1787
+ // keeps the dead incarnation's other meta (tier, pre-image
1788
+ // snapshot) intact. A vanished row or a lost fence means the
1789
+ // durable state moved under us — surface it as self-interruption
1790
+ // like the other fence losses.
1791
+ const rehomed = yield* attempts.patch(attemptId, {
1792
+ meta: attemptPayload({ ...runningMeta, ...declarationMeta, admittedBy: deps.owner })
1793
+ }, deps.owner);
1794
+ if (rehomed._tag !== "Patched") {
1795
+ return yield* Effect.interrupt;
1796
+ }
1797
+ }
1798
+ yield* emitLifecycle(JournalRecords.attemptStarted(attemptSource("started"), { ...attemptId, tier: input.tier }));
1799
+ // Shares attempt admission's transaction. Refusal rolls admission
1800
+ // back before a handler, snapshot, or sandbox can execute.
1801
+ yield* emitCallInvoked;
1802
+ }));
1803
+ /** The pre-image as the attempt row carries it: the jj operation only when reported. */
1804
+ const snapshotFields = (id, operationId) => operationId === undefined
1805
+ ? { meta: { snapshotId: id }, announced: { snapshotId: id } }
1806
+ : {
1807
+ meta: { snapshotId: id, snapshotOperationId: operationId },
1808
+ announced: { snapshotId: id, operationId }
1809
+ };
1810
+ const announceSnapshot = (fields) => emitLifecycle(JournalRecords.snapshotIdentified(attemptSource("snapshot"), { ...attemptId, ...fields.announced }));
1811
+ let snapshotId;
1812
+ let snapshotMeta = {};
1813
+ if (input.tier !== "compensable") {
1814
+ /**
1815
+ * THE TIER-2 ANCHOR FOR AN ORDINARY FRAME.
1816
+ *
1817
+ * Time travel (`packages/smithers/flows/time-travel/docs/README.md`) requires the jj pointer
1818
+ * current when a seq was journaled to be recorded at the frame,
1819
+ * because replay cannot derive it. Only compensable work took a
1820
+ * fresh snapshot, so every other frame had no anchor at all and a
1821
+ * rewind to it restored the workspace to whatever the nearest
1822
+ * *compensable* attempt happened to leave behind.
1823
+ *
1824
+ * The anchor is `carried`: it asserts "the same pointer as the
1825
+ * previous anchor in this lineage" rather than naming one. That is
1826
+ * the cheap half of the obligation — no jj call, no host round
1827
+ * trip, one journal row — and the snapshot projector resolves it by
1828
+ * copying the last change id forward. A lineage that has taken no
1829
+ * snapshot yet carries nothing forward, which is honest: there is
1830
+ * no pointer to restore, and a rewind reports none rather than
1831
+ * inventing one.
1832
+ */
1833
+ yield* emitLifecycle(JournalRecords.snapshotIdentified(attemptSource("snapshot"), { ...attemptId, carried: true }));
1834
+ }
1835
+ if (input.tier === "compensable") {
1836
+ const jj = deps.engineJj === undefined ? yield* Jj.Jj : deps.engineJj;
1837
+ /**
1838
+ * THE ENGINE'S BOUNDARY HANDLE (issue #1805). The engine restores
1839
+ * the retry sequence's original handle inside `input.snapshot` and
1840
+ * hands back this attempt's pre-image. It runs only here, past the
1841
+ * journal and cache lookups, so a replayed attempt never touches
1842
+ * the boundary. An adopted attempt keeps the handle its dead
1843
+ * incarnation persisted: that one predates any partial mutation.
1844
+ * Every meta write below carries it, so it reaches the row before
1845
+ * the body can run.
1846
+ */
1847
+ const boundaryHandle = input.snapshot === undefined ? undefined : yield* input.snapshot;
1848
+ const boundarySnapshot = runningMeta?.boundarySnapshot ?? (input.snapshot === undefined
1849
+ ? undefined
1850
+ : boundaryHandle === undefined
1851
+ ? {}
1852
+ : { handle: boundaryHandle });
1853
+ const boundaryMeta = boundarySnapshot === undefined ? {} : { boundarySnapshot };
1854
+ if (adopted && runningMeta?.snapshotId !== undefined) {
1855
+ // The dead incarnation persisted this attempt's own pre-image
1856
+ // before mutating the workspace (issue #87): restore it so the
1857
+ // re-execution runs on the clean tree, and keep it as the attempt's
1858
+ // compensation baseline instead of snapshotting the dirty state.
1859
+ yield* jj.restore(runningMeta.snapshotId);
1860
+ snapshotId = runningMeta.snapshotId;
1861
+ const fields = snapshotFields(snapshotId, runningMeta.snapshotOperationId);
1862
+ snapshotMeta = { ...fields.meta, ...boundaryMeta };
1863
+ // Re-announcing a pre-image the row already records durably:
1864
+ // there is no state write to pair this announcement with.
1865
+ yield* announceSnapshot(fields);
1866
+ }
1867
+ else {
1868
+ if (input.attempt > 1) {
1869
+ const previous = yield* attempts.get({ ...attemptId, attempt: input.attempt - 1 });
1870
+ if (Option.isSome(previous) &&
1871
+ decodeMeta(previous.value.meta)?.snapshotId !== undefined) {
1872
+ yield* jj.restore(decodeMeta(previous.value.meta).snapshotId);
1873
+ }
1874
+ }
1875
+ const snapshot = yield* jj.snapshot(`smithers action ${stepKeyDigest} attempt ${input.attempt}`);
1876
+ // The immutable commit id: a retry or an adoption restores this
1877
+ // pointer, and a change id would follow an agent's rewrite of the
1878
+ // pre-image change (`jj squash`) and restore the step's edits.
1879
+ snapshotId = snapshot.commitId;
1880
+ const fields = snapshotFields(snapshotId, snapshot.operationId);
1881
+ snapshotMeta = { ...fields.meta, ...boundaryMeta };
1882
+ // Persist the pre-image into the running row before announcing it
1883
+ // (issue #87): a SIGKILL mid-attempt must not lose the only
1884
+ // reference to the clean tree, or adoption re-executes on top of
1885
+ // the dead incarnation's partial mutations. The announcement
1886
+ // shares the patch's transaction, so the journal never names a
1887
+ // pre-image the row does not carry. The Jj snapshot itself stays
1888
+ // outside: a host call must never run inside a write transaction.
1889
+ yield* atomically(attempts.patch(attemptId, {
1890
+ meta: attemptPayload({ ...declarationMeta, admittedBy: deps.owner, ...snapshotMeta })
1891
+ }, deps.owner).pipe(Effect.andThen(announceSnapshot(fields))));
1892
+ }
1893
+ }
1894
+ /**
1895
+ * Settles an attempt its boundary refused and rethrows the refusal.
1896
+ * Preparation, sandbox open, and settlement can each refuse, and all
1897
+ * three are the same durable transition: a `failed` row marked
1898
+ * `hardViolation`, committed with the hard-violation record and the
1899
+ * failed finish. A lost fence self-interrupts instead.
1900
+ *
1901
+ * A boundary exists only for sealed work, while snapshots are
1902
+ * created only for compensable work. The two capabilities are
1903
+ * disjoint, so a refused attempt never carries a snapshot.
1904
+ */
1905
+ const failBoundaryAttempt = (cause) => Effect.gen(function* () {
1906
+ const finishedAtMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor));
1907
+ const finished = yield* settleAttempt({
1908
+ ...attemptId,
1909
+ state: "failed",
1910
+ finishedAtMs,
1911
+ error: attemptPayload(persistCause(cause)),
1912
+ meta: { ...declarationMeta, hardViolation: true }
1913
+ }, [
1914
+ JournalRecords.hardViolation(attemptSource("hard-violation"), { ...attemptId, error: cause }),
1915
+ JournalRecords.attemptFinished(attemptSource("finished"), { ...attemptId, state: "failed" })
1916
+ ]);
1917
+ if (!finished) {
1918
+ return yield* Effect.interrupt;
1919
+ }
1920
+ return yield* Effect.failCause(cause);
1921
+ });
1922
+ const boundary = input.tier === "sealed" && input.metadata !== undefined
1923
+ ? yield* StepBoundary.StepBoundary
1924
+ : undefined;
1925
+ const preparedResult = boundary === undefined || input.metadata === undefined
1926
+ ? undefined
1927
+ : yield* boundary.prepare(input.metadata).pipe(Effect.exit);
1928
+ if (preparedResult !== undefined && Exit.isFailure(preparedResult)) {
1929
+ return yield* failBoundaryAttempt(preparedResult.cause);
1930
+ }
1931
+ const prepared = preparedResult === undefined ? undefined : preparedResult.value;
1932
+ /**
1933
+ * THE ISOLATED EXECUTION (this lane). A sealed action carrying a
1934
+ * boundary descriptor runs inside a workspace transaction when one
1935
+ * is composed: the body observes only its declared read set, its
1936
+ * writes become a diff bundle, and the host is untouched until
1937
+ * copy-back. That is what makes whole-tree write verification
1938
+ * structural rather than inferred — and therefore what lets a
1939
+ * production-composed result enter the shared cache at all.
1940
+ *
1941
+ * The service is optional. Without it the body runs directly
1942
+ * against the host, exactly as before, and its evidence keeps the
1943
+ * honest omission that withholds it from the shared cache.
1944
+ */
1945
+ const stepSandbox = boundary === undefined || input.metadata === undefined
1946
+ ? Option.none()
1947
+ : yield* Effect.serviceOption(StepSandbox.StepSandbox);
1948
+ const opened = Option.isSome(stepSandbox) ? yield* stepSandbox.value.open.pipe(Effect.exit) : undefined;
1949
+ if (opened !== undefined && Exit.isFailure(opened)) {
1950
+ // A host that cannot isolate (`layerNoop`, a refusing forest) is a
1951
+ // typed refusal, not a crash: settle the attempt, or the row stays
1952
+ // "running" and reads as an abandoned attempt to the reclaim
1953
+ // machinery.
1954
+ return yield* failBoundaryAttempt(opened.cause);
1955
+ }
1956
+ const sandbox = boundary === undefined || input.metadata === undefined
1957
+ ? undefined
1958
+ : opened !== undefined
1959
+ ? opened.value
1960
+ : Option.getOrUndefined(yield* Effect.serviceOption(WorkspaceSandbox.WorkspaceSandbox));
1961
+ const isolated = sandbox === undefined || input.metadata === undefined
1962
+ ? undefined
1963
+ : yield* SandboxedExecution.execute({
1964
+ sandbox,
1965
+ descriptor: input.metadata,
1966
+ workflow: execute(input, stepKeyDigest)
1967
+ }).pipe(Effect.exit);
1968
+ // The settlement is the isolated execution's whole story; the
1969
+ // attempt's outcome is only its `result`, so the ordinary failure
1970
+ // handling below is unchanged by which path produced it.
1971
+ const settlement = isolated !== undefined && Exit.isSuccess(isolated) ? isolated.value : undefined;
1972
+ /**
1973
+ * THE EFFECT BOUNDARY. An irreversible dispatch can change the world
1974
+ * outside this journal, and a compensable one mutates the workspace
1975
+ * a rewind must restore — both are wrapped: `intended` commits
1976
+ * before the body starts, and the terminal record commits after it
1977
+ * settles — `succeeded` with the recorded result, `unknown` for a
1978
+ * failure, defect, or interruption whose external outcome nobody can
1979
+ * testify to.
1980
+ *
1981
+ * The compensable record is what makes the tier-2 restore REAL: a
1982
+ * rewind classifies the doomed suffix by its boundary rows, so a
1983
+ * compensable action that recorded only its pre-image snapshot left
1984
+ * nothing for the rewind to restore against — the suffix archived
1985
+ * "completed" while the tree kept the discarded future's bytes. The
1986
+ * record names the attempt's anchored `changeId` so the evidence and
1987
+ * the pointer travel together.
1988
+ *
1989
+ * The settlement is uninterruptible: cancellation must not strand an
1990
+ * effect that has already crossed without at least attempting to say
1991
+ * so. Sealed work is deliberately outside this — a sealed result is
1992
+ * cache evidence, and replay answers it from the recorded cache
1993
+ * entry rather than an operator decision.
1994
+ */
1995
+ const effect = input.tier === "irreversible" || input.tier === "compensable"
1996
+ ? {
1997
+ id: `${deps.runId}:${stepKeyDigest}:${input.attempt}`,
1998
+ kind: actionKind(input.action),
1999
+ tier: input.tier,
2000
+ runId: deps.runId,
2001
+ lineageId,
2002
+ sourceId: deps.sourceId,
2003
+ attempt: input.attempt,
2004
+ ...(deps.idempotencyKey === undefined ? {} : { idempotencyKey: deps.idempotencyKey }),
2005
+ ...(snapshotId === undefined ? {} : { changeId: snapshotId })
2006
+ }
2007
+ : undefined;
2008
+ /**
2009
+ * Whether a failing exit ends this dispatch in a durable PARK rather
2010
+ * than a settlement (N-08). A body that reaches a wait point marks
2011
+ * its instance suspended and interrupts, so an interrupt-only exit
2012
+ * under a suspended instance is a run that parked. Quarantine from
2013
+ * a nested action also parks its wrapper: resume must re-enter it
2014
+ * to consume the inner action's preserved outcome. Both the effect
2015
+ * boundary and the attempt row read it: a park closes neither.
2016
+ */
2017
+ const parked = (cause) => Effect.map(Effect.serviceOption(FlowRuntime.FlowInstance), (instance) => evidenceQuarantined(cause) !== undefined ||
2018
+ (Option.getOrUndefined(instance)?.suspended === true && Cause.hasInterruptsOnly(cause)));
2019
+ /**
2020
+ * The row's meta as this dispatch last wrote it. The crossing writes
2021
+ * below patch `meta` wholesale, so everything the attempt already
2022
+ * carries — the adopted incarnation's fields, the tier declaration,
2023
+ * the compensable pre-image — has to be carried across with it.
2024
+ */
2025
+ const dispatchMeta = {
2026
+ ...runningMeta,
2027
+ ...declarationMeta,
2028
+ admittedBy: deps.owner,
2029
+ ...snapshotMeta
2030
+ };
2031
+ /**
2032
+ * Commits a crossing into the attempt row, with the boundary record
2033
+ * that describes it when there is one, as ONE transaction.
2034
+ *
2035
+ * The row is what recovery reads (see `AttemptMeta.effectCrossing`),
2036
+ * so a crash must never leave the two disagreeing: a record without
2037
+ * the row would re-dispatch a crossed effect, and a row without the
2038
+ * record would settle a run from evidence the audit cannot show. A
2039
+ * refused patch is a lost fence, surfaced as self-interruption like
2040
+ * every other fence outcome here, and reported from OUTSIDE the
2041
+ * transaction so the transaction's own failures stay storage
2042
+ * failures.
2043
+ */
2044
+ const recordCrossing = (crossing, patch, record) => Effect.flatMap(atomically(Effect.gen(function* () {
2045
+ if (record !== undefined) {
2046
+ yield* emitLifecycle(record);
2047
+ }
2048
+ const patched = yield* attempts.patch(attemptId, {
2049
+ ...patch,
2050
+ meta: attemptPayload({ ...dispatchMeta, effectCrossing: crossing })
2051
+ }, deps.owner);
2052
+ return patched._tag === "Patched";
2053
+ })), (recorded) => recorded ? Effect.void : Effect.interrupt);
2054
+ const dispatch = effect === undefined
2055
+ ? execute(input, stepKeyDigest)
2056
+ : Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
2057
+ yield* recordCrossing("intended", {}, EffectRecords.boundary(effect, "intended"));
2058
+ const exit = yield* Effect.exit(restore(execute(input, stepKeyDigest)));
2059
+ yield* Exit.isSuccess(exit)
2060
+ ? recordCrossing("succeeded", { outcome: attemptPayload(exit.value) }, EffectRecords.boundary(effect, "succeeded", exit.value))
2061
+ // A park leaves the boundary OPEN. The terminal record says
2062
+ // the effect finished and nobody can testify to how; a parked
2063
+ // dispatch has not finished, and the next drive re-enters the
2064
+ // SAME attempt. Writing it here made a routine park and
2065
+ // resume read `intended, unknown, intended, succeeded` in the
2066
+ // journal a rewind classifies a doomed suffix from.
2067
+ //
2068
+ // The park is still recorded in the ROW, with no record to
2069
+ // pair it with: a park and a SIGKILL mid-body leave the same
2070
+ // `running` row and the same open boundary, and only this
2071
+ // says which one happened. Without it the resumed wait looked
2072
+ // like an unresolved crossing and the refusal above made every
2073
+ // keyless irreversible wait unresumable.
2074
+ : Effect.flatMap(parked(exit.cause), (isPark) => isPark
2075
+ ? recordCrossing("parked", {})
2076
+ : emitLifecycle(EffectRecords.boundary(effect, "unknown")).pipe(Effect.catchCause((cause) => Effect.logWarning("engine-store: failed to persist unknown effect boundary", cause).pipe(Effect.annotateLogs({ runId: deps.runId, step: input.key })))));
2077
+ return yield* exit;
2078
+ }));
2079
+ const outcome = isolated === undefined
2080
+ ? yield* dispatch.pipe(Effect.exit)
2081
+ : Exit.map(isolated, (settled) => settled.result);
2082
+ if (Exit.isFailure(outcome)) {
2083
+ /**
2084
+ * A DURABLE PARK IS NOT A SETTLEMENT (N-08). A body that reaches a
2085
+ * wait point ends its fiber by self-interruption — `Flow.suspend`
2086
+ * marks the instance and interrupts — so an interrupt-only exit
2087
+ * from a dispatch whose instance is suspended describes a run that
2088
+ * parked, not an attempt that failed. Settling it below wrote a
2089
+ * `failed` row carrying the interrupt cause, and the replay branch
2090
+ * above then rethrew that cause on every later drive: the run died
2091
+ * with "All fibers interrupted without error" instead of resuming.
2092
+ * The memory engine kept no attempt row and so never saw it, which
2093
+ * is why `HumanTask.timeoutMs`, `Action.raceAll`, and every durable
2094
+ * wait raced against a timer worked in memory and not on SQLite.
2095
+ *
2096
+ * The row therefore stays `running`, which is exactly the shape the
2097
+ * adoption path above re-enters: the next drive re-executes the
2098
+ * body under the SAME attempt number, so the park costs nothing
2099
+ * against the retry budget and the raced deferreds re-register
2100
+ * against their persisted completions. `running` is also the only
2101
+ * in-progress state the attempt store admits, so a later settlement
2102
+ * of this attempt is an ordinary fenced `finish`.
2103
+ */
2104
+ if (yield* parked(outcome.cause)) {
2105
+ return yield* Effect.failCause(outcome.cause);
2106
+ }
2107
+ const finishedAtMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor));
2108
+ // A boundary violation raised while the body ran is classified
2109
+ // like one raised at settle time (issue #109): the row records it
2110
+ // so a post-crash replay can re-emit the violation record, which
2111
+ // the persisted cause alone cannot distinguish.
2112
+ const violation = declarationViolated(outcome.cause);
2113
+ const finished = yield* settleAttempt({
2114
+ ...attemptId,
2115
+ state: "failed",
2116
+ finishedAtMs,
2117
+ error: attemptPayload(persistCause(outcome.cause)),
2118
+ meta: attemptPayload({
2119
+ ...declarationMeta,
2120
+ ...(violation ? { hardViolation: true } : {}),
2121
+ ...snapshotMeta
2122
+ })
2123
+ }, [
2124
+ ...(violation
2125
+ ? [
2126
+ JournalRecords.hardViolation(attemptSource("hard-violation"), {
2127
+ ...attemptId,
2128
+ error: outcome.cause
2129
+ })
2130
+ ]
2131
+ : []),
2132
+ JournalRecords.attemptFinished(attemptSource("finished"), { ...attemptId, state: "failed" })
2133
+ ]);
2134
+ if (!finished) {
2135
+ return yield* Effect.interrupt;
2136
+ }
2137
+ return yield* Effect.failCause(outcome.cause);
2138
+ }
2139
+ if (settlement !== undefined) {
2140
+ // Forensics requires both halves as journal facts, never inferred
2141
+ // from an absence: what the
2142
+ // transaction proposed, and that it reached the host.
2143
+ yield* emitConverging(JournalRecords.diffBundleCaptured(attemptSource("diff-bundle"), {
2144
+ ...attemptId,
2145
+ bundleIdentity: settlement.bundleIdentity,
2146
+ changedPaths: settlement.files.map((change) => change.path),
2147
+ deviations: settlement.deviations
2148
+ }));
2149
+ // THE DISPATCH STAGE. Queued effects fire here — after copy-back
2150
+ // settled, outside the transaction, deduplicated by idempotency
2151
+ // key so a body that queued the same key twice, and a bundle that
2152
+ // rebased before it landed, both send exactly once.
2153
+ const dispatcher = Option.getOrUndefined(yield* Effect.serviceOption(WorkspaceSandbox.EffectDispatcher));
2154
+ const deduped = new Set();
2155
+ for (const queued of settlement.effects) {
2156
+ if (deduped.has(queued.idempotencyKey))
2157
+ continue;
2158
+ deduped.add(queued.idempotencyKey);
2159
+ if (dispatcher !== undefined)
2160
+ yield* dispatcher.dispatch(queued);
2161
+ }
2162
+ yield* emitConverging(JournalRecords.copyBackSettled(attemptSource("copy-back"), {
2163
+ ...attemptId,
2164
+ bundleIdentity: settlement.bundleIdentity,
2165
+ rebases: settlement.rebases,
2166
+ queued: [...deduped],
2167
+ dispatched: dispatcher === undefined ? [] : [...deduped]
2168
+ }));
2169
+ }
2170
+ const settled = prepared === undefined || boundary === undefined
2171
+ ? undefined
2172
+ : yield* boundary.settle(prepared).pipe(Effect.exit);
2173
+ yield* settled === undefined ? Effect.void : Metric.update(EngineStoreMetrics.boundarySettlement[Exit.isSuccess(settled)
2174
+ ? settled.value.deviation === undefined ? "Clean" : "Deviation"
2175
+ : settlementViolated(settled.cause)
2176
+ ? "Violation"
2177
+ : "Refused"], 1);
2178
+ if (settled !== undefined && Exit.isFailure(settled)) {
2179
+ return yield* failBoundaryAttempt(settled.cause);
2180
+ }
2181
+ const settledEvidence = settled === undefined ? undefined : settled.value;
2182
+ /**
2183
+ * THE RETIRED LIMITATION. `StepBoundary`'s filesystem layer omits
2184
+ * `wholeTreeWritesVerified` because it can only re-measure paths it
2185
+ * was told about, and only `layerTest` ever set it — so under the
2186
+ * production composition nothing could enter the shared cache.
2187
+ *
2188
+ * An isolated execution answers the question the boundary could not:
2189
+ * the transaction *is* the tree, so a write outside the declared set
2190
+ * is a map comparison rather than an inference. The claim is made
2191
+ * here, by the code that knows the body ran in isolation, and only
2192
+ * when the whole-tree diff found no deviation — the same whole-tree
2193
+ * view also supplies a deviation the boundary's declared-read scan
2194
+ * would have missed entirely.
2195
+ */
2196
+ const evidence = settledEvidence === undefined || settlement === undefined
2197
+ ? settledEvidence
2198
+ : {
2199
+ ...settledEvidence,
2200
+ ...(settlement.deviations.length === 0
2201
+ ? {
2202
+ wholeTreeWritesVerified: true,
2203
+ // `StepSandbox` is the injectable façade; the
2204
+ // `WorkspaceSandbox` service is the same isolated
2205
+ // transaction backend and proves the same read property.
2206
+ hermeticReadsVerified: true
2207
+ }
2208
+ : {
2209
+ deviation: {
2210
+ _tag: "ExpectedSetDeviation",
2211
+ paths: settlement.deviations,
2212
+ diffIdentity: settlement.bundleIdentity
2213
+ }
2214
+ })
2215
+ };
2216
+ const finishedAtMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor));
2217
+ // The declared read set is the key input; the prepare-time
2218
+ // measurement is the evidence it described reality when the body
2219
+ // ran (issue #106). A mismatch means the result was computed from
2220
+ // different inputs than the key claims — the attempt itself is
2221
+ // fine, but the completion must never enter the shared cache.
2222
+ const readSetVerified = prepared !== undefined && StepBoundary.readSetMatches(prepared);
2223
+ const meta = {
2224
+ ...declarationMeta,
2225
+ ...snapshotMeta,
2226
+ ...(evidence === undefined ? {} : { boundary: evidence }),
2227
+ ...(readSetVerified ? { readSetVerified: true } : {})
2228
+ };
2229
+ const finished = yield* settleAttempt({
2230
+ ...attemptId,
2231
+ state: "succeeded",
2232
+ finishedAtMs,
2233
+ outcome: attemptPayload(outcome.value),
2234
+ meta: attemptPayload(meta)
2235
+ }, [
2236
+ ...(evidence?.deviation === undefined ? [] : [
2237
+ JournalRecords.expectedSetDeviation(attemptSource("deviation"), {
2238
+ ...attemptId,
2239
+ ...evidence.deviation
2240
+ })
2241
+ ]),
2242
+ JournalRecords.attemptFinished(attemptSource("finished"), { ...attemptId, state: "succeeded" })
2243
+ ]);
2244
+ if (!finished)
2245
+ return yield* Effect.interrupt;
2246
+ const admission = CacheAdmission.completion(cacheDeclaration, meta, false);
2247
+ if (admission._tag === "PublishCompletion") {
2248
+ yield* recordCache({ result: outcome.value, admission, createdAtMs: finishedAtMs });
2249
+ }
2250
+ else if (admission.reason === "unverified-recorded-reads") {
2251
+ // Visible, not silent (issue #106): the run continues on its
2252
+ // own result, but the stale declaration is journalled so the
2253
+ // missing cache entry is explainable.
2254
+ yield* emitConverging(JournalRecords.cacheProvenance(cacheSource("unverified_read_set"), {
2255
+ keyDigest,
2256
+ action: "unverified_read_set"
2257
+ }));
2258
+ }
2259
+ return outcome.value;
2260
+ }));
2261
+ }).pipe(
2262
+ // The operation's own span is annotated above once the digest exists;
2263
+ // this ambient context gives every child store, boundary, and sandbox
2264
+ // span the dispatch identity as it opens.
2265
+ //
2266
+ // `stepKey`, never `key`: `@smthrs/journal`'s `Redaction.isSensitiveKey`
2267
+ // treats a standalone trailing `key` word as a credential name and
2268
+ // replaces the value, and `RedactedLogger` applies that rule to every
2269
+ // log annotation under a real host. The span uses the same name so a
2270
+ // trace and a log line name the dispatch identically.
2271
+ Effect.annotateSpans({
2272
+ runId: deps.runId,
2273
+ stepKey: input.key,
2274
+ attempt: input.attempt,
2275
+ tier: input.tier
2276
+ }), Effect.annotateLogs({
2277
+ runId: deps.runId,
2278
+ stepKey: input.key,
2279
+ attempt: input.attempt,
2280
+ tier: input.tier
2281
+ }), EngineStoreMetrics.observe({
2282
+ timer: EngineStoreMetrics.dispatchDuration,
2283
+ counter: EngineStoreMetrics.dispatch
2284
+ }),
2285
+ // A dispatch that settled without reaching the gate was served from a
2286
+ // durable record: a recorded success, or a recorded failure it rethrew.
2287
+ // An interruption or a defect settled nothing and reports nothing.
2288
+ Effect.onExit((exit) => {
2289
+ const replayOnly = deps.replayOnly;
2290
+ const stepKeyDigest = replay.stepKeyDigest;
2291
+ if (replayOnly === undefined || replay.gated || stepKeyDigest === undefined)
2292
+ return Effect.void;
2293
+ if (Exit.isFailure(exit) && (Cause.hasInterrupts(exit.cause) || Cause.hasDies(exit.cause)))
2294
+ return Effect.void;
2295
+ return replayOnly.observe({
2296
+ runId: deps.runId,
2297
+ stepKeyDigest,
2298
+ attempt: input.attempt,
2299
+ action: actionKind(input.action),
2300
+ tier: input.tier,
2301
+ outcome: "replayed"
2302
+ });
2303
+ }));
2304
+ });
2305
+ };
2306
+ //# sourceMappingURL=ActionPersistence.js.map