@smthrs/control 0.0.0-stage → 1.0.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (418) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/LICENSE +21 -0
  3. package/README.md +168 -2
  4. package/dist/cjs/ApprovalAuthority.d.ts +73 -0
  5. package/dist/cjs/ApprovalAuthority.d.ts.map +1 -0
  6. package/dist/cjs/ApprovalAuthority.js +62 -0
  7. package/dist/cjs/ApprovalAuthority.js.map +7 -0
  8. package/dist/cjs/Cancellation.d.ts +107 -0
  9. package/dist/cjs/Cancellation.d.ts.map +1 -0
  10. package/dist/cjs/Cancellation.js +72 -0
  11. package/dist/cjs/Cancellation.js.map +7 -0
  12. package/dist/cjs/Channels.d.ts +170 -0
  13. package/dist/cjs/Channels.d.ts.map +1 -0
  14. package/dist/cjs/Channels.js +278 -0
  15. package/dist/cjs/Channels.js.map +7 -0
  16. package/dist/cjs/Control.d.ts +202 -0
  17. package/dist/cjs/Control.d.ts.map +1 -0
  18. package/dist/cjs/Control.js +47 -0
  19. package/dist/cjs/Control.js.map +7 -0
  20. package/dist/cjs/ControlClient.d.ts +52 -0
  21. package/dist/cjs/ControlClient.d.ts.map +1 -0
  22. package/dist/cjs/ControlClient.js +191 -0
  23. package/dist/cjs/ControlClient.js.map +7 -0
  24. package/dist/cjs/ControlError.d.ts +318 -0
  25. package/dist/cjs/ControlError.d.ts.map +1 -0
  26. package/dist/cjs/ControlError.js +249 -0
  27. package/dist/cjs/ControlError.js.map +7 -0
  28. package/dist/cjs/ControlExecutor.d.ts +372 -0
  29. package/dist/cjs/ControlExecutor.d.ts.map +1 -0
  30. package/dist/cjs/ControlExecutor.js +123 -0
  31. package/dist/cjs/ControlExecutor.js.map +7 -0
  32. package/dist/cjs/ControlFacts.d.ts +454 -0
  33. package/dist/cjs/ControlFacts.d.ts.map +1 -0
  34. package/dist/cjs/ControlFacts.js +261 -0
  35. package/dist/cjs/ControlFacts.js.map +7 -0
  36. package/dist/cjs/ControlLive.d.ts +23 -0
  37. package/dist/cjs/ControlLive.d.ts.map +1 -0
  38. package/dist/cjs/ControlLive.js +1293 -0
  39. package/dist/cjs/ControlLive.js.map +7 -0
  40. package/dist/cjs/ControlRpcs.d.ts +1204 -0
  41. package/dist/cjs/ControlRpcs.d.ts.map +1 -0
  42. package/dist/cjs/ControlRpcs.js +247 -0
  43. package/dist/cjs/ControlRpcs.js.map +7 -0
  44. package/dist/cjs/ControlRuntime.d.ts +635 -0
  45. package/dist/cjs/ControlRuntime.d.ts.map +1 -0
  46. package/dist/cjs/ControlRuntime.js +744 -0
  47. package/dist/cjs/ControlRuntime.js.map +7 -0
  48. package/dist/cjs/ControlSchema.d.ts +2642 -0
  49. package/dist/cjs/ControlSchema.d.ts.map +1 -0
  50. package/dist/cjs/ControlSchema.js +634 -0
  51. package/dist/cjs/ControlSchema.js.map +7 -0
  52. package/dist/cjs/ControlServer.d.ts +51 -0
  53. package/dist/cjs/ControlServer.d.ts.map +1 -0
  54. package/dist/cjs/ControlServer.js +121 -0
  55. package/dist/cjs/ControlServer.js.map +7 -0
  56. package/dist/cjs/Credential.d.ts +136 -0
  57. package/dist/cjs/Credential.d.ts.map +1 -0
  58. package/dist/cjs/Credential.js +168 -0
  59. package/dist/cjs/Credential.js.map +7 -0
  60. package/dist/cjs/CredentialCipher.d.ts +90 -0
  61. package/dist/cjs/CredentialCipher.d.ts.map +1 -0
  62. package/dist/cjs/CredentialCipher.js +45 -0
  63. package/dist/cjs/CredentialCipher.js.map +7 -0
  64. package/dist/cjs/CredentialStore.d.ts +97 -0
  65. package/dist/cjs/CredentialStore.d.ts.map +1 -0
  66. package/dist/cjs/CredentialStore.js +81 -0
  67. package/dist/cjs/CredentialStore.js.map +7 -0
  68. package/dist/cjs/DispatchReader.d.ts +112 -0
  69. package/dist/cjs/DispatchReader.d.ts.map +1 -0
  70. package/dist/cjs/DispatchReader.js +45 -0
  71. package/dist/cjs/DispatchReader.js.map +7 -0
  72. package/dist/cjs/Health.d.ts +333 -0
  73. package/dist/cjs/Health.d.ts.map +1 -0
  74. package/dist/cjs/Health.js +311 -0
  75. package/dist/cjs/Health.js.map +7 -0
  76. package/dist/cjs/JevSessionChecker.d.ts +57 -0
  77. package/dist/cjs/JevSessionChecker.d.ts.map +1 -0
  78. package/dist/cjs/JevSessionChecker.js +113 -0
  79. package/dist/cjs/JevSessionChecker.js.map +7 -0
  80. package/dist/cjs/Lineage.d.ts +131 -0
  81. package/dist/cjs/Lineage.d.ts.map +1 -0
  82. package/dist/cjs/Lineage.js +81 -0
  83. package/dist/cjs/Lineage.js.map +7 -0
  84. package/dist/cjs/Migrations.d.ts +34 -0
  85. package/dist/cjs/Migrations.d.ts.map +1 -0
  86. package/dist/cjs/Migrations.js +60 -0
  87. package/dist/cjs/Migrations.js.map +7 -0
  88. package/dist/cjs/Monitor.d.ts +282 -0
  89. package/dist/cjs/Monitor.d.ts.map +1 -0
  90. package/dist/cjs/Monitor.js +283 -0
  91. package/dist/cjs/Monitor.js.map +7 -0
  92. package/dist/cjs/ScopedToken.d.ts +193 -0
  93. package/dist/cjs/ScopedToken.d.ts.map +1 -0
  94. package/dist/cjs/ScopedToken.js +135 -0
  95. package/dist/cjs/ScopedToken.js.map +7 -0
  96. package/dist/cjs/SqlControlRuntime.d.ts +161 -0
  97. package/dist/cjs/SqlControlRuntime.d.ts.map +1 -0
  98. package/dist/cjs/SqlControlRuntime.js +1522 -0
  99. package/dist/cjs/SqlControlRuntime.js.map +7 -0
  100. package/dist/cjs/SqlCredentialStore.d.ts +43 -0
  101. package/dist/cjs/SqlCredentialStore.d.ts.map +1 -0
  102. package/dist/cjs/SqlCredentialStore.js +113 -0
  103. package/dist/cjs/SqlCredentialStore.js.map +7 -0
  104. package/dist/cjs/Steering.d.ts +69 -0
  105. package/dist/cjs/Steering.d.ts.map +1 -0
  106. package/dist/cjs/Steering.js +49 -0
  107. package/dist/cjs/Steering.js.map +7 -0
  108. package/dist/cjs/SystemFlows.d.ts +223 -0
  109. package/dist/cjs/SystemFlows.d.ts.map +1 -0
  110. package/dist/cjs/SystemFlows.js +195 -0
  111. package/dist/cjs/SystemFlows.js.map +7 -0
  112. package/dist/cjs/WebCryptoCipher.d.ts +49 -0
  113. package/dist/cjs/WebCryptoCipher.d.ts.map +1 -0
  114. package/dist/cjs/WebCryptoCipher.js +129 -0
  115. package/dist/cjs/WebCryptoCipher.js.map +7 -0
  116. package/dist/cjs/WebhookChannel.d.ts +113 -0
  117. package/dist/cjs/WebhookChannel.d.ts.map +1 -0
  118. package/dist/cjs/WebhookChannel.js +98 -0
  119. package/dist/cjs/WebhookChannel.js.map +7 -0
  120. package/dist/cjs/index.d.ts +160 -0
  121. package/dist/cjs/index.d.ts.map +1 -0
  122. package/dist/cjs/index.js +91 -0
  123. package/dist/cjs/index.js.map +7 -0
  124. package/dist/cjs/internal/MutationBoundary.d.ts +27 -0
  125. package/dist/cjs/internal/MutationBoundary.d.ts.map +1 -0
  126. package/dist/cjs/internal/MutationBoundary.js +50 -0
  127. package/dist/cjs/internal/MutationBoundary.js.map +7 -0
  128. package/dist/cjs/internal/activeFibers.d.ts +12 -0
  129. package/dist/cjs/internal/activeFibers.d.ts.map +1 -0
  130. package/dist/cjs/internal/activeFibers.js +30 -0
  131. package/dist/cjs/internal/activeFibers.js.map +7 -0
  132. package/dist/cjs/internal/issues.d.ts +28 -0
  133. package/dist/cjs/internal/issues.d.ts.map +1 -0
  134. package/dist/cjs/internal/issues.js +34 -0
  135. package/dist/cjs/internal/issues.js.map +7 -0
  136. package/dist/cjs/internal/planning.d.ts +347 -0
  137. package/dist/cjs/internal/planning.d.ts.map +1 -0
  138. package/dist/cjs/internal/planning.js +137 -0
  139. package/dist/cjs/internal/planning.js.map +7 -0
  140. package/dist/cjs/internal/sqlSchemaErrors.d.ts +18 -0
  141. package/dist/cjs/internal/sqlSchemaErrors.d.ts.map +1 -0
  142. package/dist/cjs/internal/sqlSchemaErrors.js +40 -0
  143. package/dist/cjs/internal/sqlSchemaErrors.js.map +7 -0
  144. package/dist/cjs/migrations/0001_control_tables.d.ts +18 -0
  145. package/dist/cjs/migrations/0001_control_tables.d.ts.map +1 -0
  146. package/dist/cjs/migrations/0001_control_tables.js +115 -0
  147. package/dist/cjs/migrations/0001_control_tables.js.map +7 -0
  148. package/dist/cjs/migrations/0002_run_keys.d.ts +15 -0
  149. package/dist/cjs/migrations/0002_run_keys.d.ts.map +1 -0
  150. package/dist/cjs/migrations/0002_run_keys.js +44 -0
  151. package/dist/cjs/migrations/0002_run_keys.js.map +7 -0
  152. package/dist/cjs/migrations/0003_signal_commands.d.ts +13 -0
  153. package/dist/cjs/migrations/0003_signal_commands.d.ts.map +1 -0
  154. package/dist/cjs/migrations/0003_signal_commands.js +50 -0
  155. package/dist/cjs/migrations/0003_signal_commands.js.map +7 -0
  156. package/dist/cjs/migrations/0004_approval_decisions.d.ts +13 -0
  157. package/dist/cjs/migrations/0004_approval_decisions.d.ts.map +1 -0
  158. package/dist/cjs/migrations/0004_approval_decisions.js +45 -0
  159. package/dist/cjs/migrations/0004_approval_decisions.js.map +7 -0
  160. package/dist/cjs/migrations/0005_signal_principals.d.ts +18 -0
  161. package/dist/cjs/migrations/0005_signal_principals.d.ts.map +1 -0
  162. package/dist/cjs/migrations/0005_signal_principals.js +45 -0
  163. package/dist/cjs/migrations/0005_signal_principals.js.map +7 -0
  164. package/dist/cjs/migrations/0006_run_principals.d.ts +18 -0
  165. package/dist/cjs/migrations/0006_run_principals.d.ts.map +1 -0
  166. package/dist/cjs/migrations/0006_run_principals.js +49 -0
  167. package/dist/cjs/migrations/0006_run_principals.js.map +7 -0
  168. package/dist/cjs/migrations/0007_resume_consent.d.ts +18 -0
  169. package/dist/cjs/migrations/0007_resume_consent.d.ts.map +1 -0
  170. package/dist/cjs/migrations/0007_resume_consent.js +46 -0
  171. package/dist/cjs/migrations/0007_resume_consent.js.map +7 -0
  172. package/dist/cjs/package.json +1 -0
  173. package/dist/cjs/test/TestControl.d.ts +19 -0
  174. package/dist/cjs/test/TestControl.d.ts.map +1 -0
  175. package/dist/cjs/test/TestControl.js +62 -0
  176. package/dist/cjs/test/TestControl.js.map +7 -0
  177. package/dist/esm/ApprovalAuthority.d.ts +73 -0
  178. package/dist/esm/ApprovalAuthority.d.ts.map +1 -0
  179. package/dist/esm/ApprovalAuthority.js +72 -0
  180. package/dist/esm/ApprovalAuthority.js.map +1 -0
  181. package/dist/esm/Cancellation.d.ts +107 -0
  182. package/dist/esm/Cancellation.d.ts.map +1 -0
  183. package/dist/esm/Cancellation.js +116 -0
  184. package/dist/esm/Cancellation.js.map +1 -0
  185. package/dist/esm/Channels.d.ts +170 -0
  186. package/dist/esm/Channels.d.ts.map +1 -0
  187. package/dist/esm/Channels.js +312 -0
  188. package/dist/esm/Channels.js.map +1 -0
  189. package/dist/esm/Control.d.ts +202 -0
  190. package/dist/esm/Control.d.ts.map +1 -0
  191. package/dist/esm/Control.js +42 -0
  192. package/dist/esm/Control.js.map +1 -0
  193. package/dist/esm/ControlClient.d.ts +52 -0
  194. package/dist/esm/ControlClient.d.ts.map +1 -0
  195. package/dist/esm/ControlClient.js +217 -0
  196. package/dist/esm/ControlClient.js.map +1 -0
  197. package/dist/esm/ControlError.d.ts +318 -0
  198. package/dist/esm/ControlError.d.ts.map +1 -0
  199. package/dist/esm/ControlError.js +359 -0
  200. package/dist/esm/ControlError.js.map +1 -0
  201. package/dist/esm/ControlExecutor.d.ts +372 -0
  202. package/dist/esm/ControlExecutor.d.ts.map +1 -0
  203. package/dist/esm/ControlExecutor.js +212 -0
  204. package/dist/esm/ControlExecutor.js.map +1 -0
  205. package/dist/esm/ControlFacts.d.ts +454 -0
  206. package/dist/esm/ControlFacts.d.ts.map +1 -0
  207. package/dist/esm/ControlFacts.js +324 -0
  208. package/dist/esm/ControlFacts.js.map +1 -0
  209. package/dist/esm/ControlLive.d.ts +23 -0
  210. package/dist/esm/ControlLive.d.ts.map +1 -0
  211. package/dist/esm/ControlLive.js +1589 -0
  212. package/dist/esm/ControlLive.js.map +1 -0
  213. package/dist/esm/ControlRpcs.d.ts +1204 -0
  214. package/dist/esm/ControlRpcs.d.ts.map +1 -0
  215. package/dist/esm/ControlRpcs.js +299 -0
  216. package/dist/esm/ControlRpcs.js.map +1 -0
  217. package/dist/esm/ControlRuntime.d.ts +635 -0
  218. package/dist/esm/ControlRuntime.d.ts.map +1 -0
  219. package/dist/esm/ControlRuntime.js +808 -0
  220. package/dist/esm/ControlRuntime.js.map +1 -0
  221. package/dist/esm/ControlSchema.d.ts +2642 -0
  222. package/dist/esm/ControlSchema.d.ts.map +1 -0
  223. package/dist/esm/ControlSchema.js +1030 -0
  224. package/dist/esm/ControlSchema.js.map +1 -0
  225. package/dist/esm/ControlServer.d.ts +51 -0
  226. package/dist/esm/ControlServer.d.ts.map +1 -0
  227. package/dist/esm/ControlServer.js +145 -0
  228. package/dist/esm/ControlServer.js.map +1 -0
  229. package/dist/esm/Credential.d.ts +136 -0
  230. package/dist/esm/Credential.d.ts.map +1 -0
  231. package/dist/esm/Credential.js +190 -0
  232. package/dist/esm/Credential.js.map +1 -0
  233. package/dist/esm/CredentialCipher.d.ts +90 -0
  234. package/dist/esm/CredentialCipher.d.ts.map +1 -0
  235. package/dist/esm/CredentialCipher.js +56 -0
  236. package/dist/esm/CredentialCipher.js.map +1 -0
  237. package/dist/esm/CredentialStore.d.ts +97 -0
  238. package/dist/esm/CredentialStore.d.ts.map +1 -0
  239. package/dist/esm/CredentialStore.js +101 -0
  240. package/dist/esm/CredentialStore.js.map +1 -0
  241. package/dist/esm/DispatchReader.d.ts +112 -0
  242. package/dist/esm/DispatchReader.d.ts.map +1 -0
  243. package/dist/esm/DispatchReader.js +76 -0
  244. package/dist/esm/DispatchReader.js.map +1 -0
  245. package/dist/esm/Health.d.ts +333 -0
  246. package/dist/esm/Health.d.ts.map +1 -0
  247. package/dist/esm/Health.js +400 -0
  248. package/dist/esm/Health.js.map +1 -0
  249. package/dist/esm/JevSessionChecker.d.ts +57 -0
  250. package/dist/esm/JevSessionChecker.d.ts.map +1 -0
  251. package/dist/esm/JevSessionChecker.js +108 -0
  252. package/dist/esm/JevSessionChecker.js.map +1 -0
  253. package/dist/esm/Lineage.d.ts +131 -0
  254. package/dist/esm/Lineage.d.ts.map +1 -0
  255. package/dist/esm/Lineage.js +174 -0
  256. package/dist/esm/Lineage.js.map +1 -0
  257. package/dist/esm/Migrations.d.ts +34 -0
  258. package/dist/esm/Migrations.d.ts.map +1 -0
  259. package/dist/esm/Migrations.js +53 -0
  260. package/dist/esm/Migrations.js.map +1 -0
  261. package/dist/esm/Monitor.d.ts +282 -0
  262. package/dist/esm/Monitor.d.ts.map +1 -0
  263. package/dist/esm/Monitor.js +415 -0
  264. package/dist/esm/Monitor.js.map +1 -0
  265. package/dist/esm/ScopedToken.d.ts +193 -0
  266. package/dist/esm/ScopedToken.d.ts.map +1 -0
  267. package/dist/esm/ScopedToken.js +224 -0
  268. package/dist/esm/ScopedToken.js.map +1 -0
  269. package/dist/esm/SqlControlRuntime.d.ts +161 -0
  270. package/dist/esm/SqlControlRuntime.d.ts.map +1 -0
  271. package/dist/esm/SqlControlRuntime.js +1756 -0
  272. package/dist/esm/SqlControlRuntime.js.map +1 -0
  273. package/dist/esm/SqlCredentialStore.d.ts +43 -0
  274. package/dist/esm/SqlCredentialStore.d.ts.map +1 -0
  275. package/dist/esm/SqlCredentialStore.js +97 -0
  276. package/dist/esm/SqlCredentialStore.js.map +1 -0
  277. package/dist/esm/Steering.d.ts +69 -0
  278. package/dist/esm/Steering.d.ts.map +1 -0
  279. package/dist/esm/Steering.js +89 -0
  280. package/dist/esm/Steering.js.map +1 -0
  281. package/dist/esm/SystemFlows.d.ts +223 -0
  282. package/dist/esm/SystemFlows.d.ts.map +1 -0
  283. package/dist/esm/SystemFlows.js +198 -0
  284. package/dist/esm/SystemFlows.js.map +1 -0
  285. package/dist/esm/WebCryptoCipher.d.ts +49 -0
  286. package/dist/esm/WebCryptoCipher.d.ts.map +1 -0
  287. package/dist/esm/WebCryptoCipher.js +123 -0
  288. package/dist/esm/WebCryptoCipher.js.map +1 -0
  289. package/dist/esm/WebhookChannel.d.ts +113 -0
  290. package/dist/esm/WebhookChannel.d.ts.map +1 -0
  291. package/dist/esm/WebhookChannel.js +109 -0
  292. package/dist/esm/WebhookChannel.js.map +1 -0
  293. package/dist/esm/index.d.ts +160 -0
  294. package/dist/esm/index.d.ts.map +1 -0
  295. package/dist/esm/index.js +160 -0
  296. package/dist/esm/index.js.map +1 -0
  297. package/dist/esm/internal/MutationBoundary.d.ts +27 -0
  298. package/dist/esm/internal/MutationBoundary.d.ts.map +1 -0
  299. package/dist/esm/internal/MutationBoundary.js +40 -0
  300. package/dist/esm/internal/MutationBoundary.js.map +1 -0
  301. package/dist/esm/internal/activeFibers.d.ts +12 -0
  302. package/dist/esm/internal/activeFibers.d.ts.map +1 -0
  303. package/dist/esm/internal/activeFibers.js +17 -0
  304. package/dist/esm/internal/activeFibers.js.map +1 -0
  305. package/dist/esm/internal/issues.d.ts +28 -0
  306. package/dist/esm/internal/issues.d.ts.map +1 -0
  307. package/dist/esm/internal/issues.js +35 -0
  308. package/dist/esm/internal/issues.js.map +1 -0
  309. package/dist/esm/internal/planning.d.ts +347 -0
  310. package/dist/esm/internal/planning.d.ts.map +1 -0
  311. package/dist/esm/internal/planning.js +199 -0
  312. package/dist/esm/internal/planning.js.map +1 -0
  313. package/dist/esm/internal/sqlSchemaErrors.d.ts +18 -0
  314. package/dist/esm/internal/sqlSchemaErrors.d.ts.map +1 -0
  315. package/dist/esm/internal/sqlSchemaErrors.js +38 -0
  316. package/dist/esm/internal/sqlSchemaErrors.js.map +1 -0
  317. package/dist/esm/migrations/0001_control_tables.d.ts +18 -0
  318. package/dist/esm/migrations/0001_control_tables.d.ts.map +1 -0
  319. package/dist/esm/migrations/0001_control_tables.js +96 -0
  320. package/dist/esm/migrations/0001_control_tables.js.map +1 -0
  321. package/dist/esm/migrations/0002_run_keys.d.ts +15 -0
  322. package/dist/esm/migrations/0002_run_keys.d.ts.map +1 -0
  323. package/dist/esm/migrations/0002_run_keys.js +22 -0
  324. package/dist/esm/migrations/0002_run_keys.js.map +1 -0
  325. package/dist/esm/migrations/0003_signal_commands.d.ts +13 -0
  326. package/dist/esm/migrations/0003_signal_commands.d.ts.map +1 -0
  327. package/dist/esm/migrations/0003_signal_commands.js +26 -0
  328. package/dist/esm/migrations/0003_signal_commands.js.map +1 -0
  329. package/dist/esm/migrations/0004_approval_decisions.d.ts +13 -0
  330. package/dist/esm/migrations/0004_approval_decisions.d.ts.map +1 -0
  331. package/dist/esm/migrations/0004_approval_decisions.js +25 -0
  332. package/dist/esm/migrations/0004_approval_decisions.js.map +1 -0
  333. package/dist/esm/migrations/0005_signal_principals.d.ts +18 -0
  334. package/dist/esm/migrations/0005_signal_principals.d.ts.map +1 -0
  335. package/dist/esm/migrations/0005_signal_principals.js +27 -0
  336. package/dist/esm/migrations/0005_signal_principals.js.map +1 -0
  337. package/dist/esm/migrations/0006_run_principals.d.ts +18 -0
  338. package/dist/esm/migrations/0006_run_principals.d.ts.map +1 -0
  339. package/dist/esm/migrations/0006_run_principals.js +30 -0
  340. package/dist/esm/migrations/0006_run_principals.js.map +1 -0
  341. package/dist/esm/migrations/0007_resume_consent.d.ts +18 -0
  342. package/dist/esm/migrations/0007_resume_consent.d.ts.map +1 -0
  343. package/dist/esm/migrations/0007_resume_consent.js +27 -0
  344. package/dist/esm/migrations/0007_resume_consent.js.map +1 -0
  345. package/dist/esm/test/TestControl.d.ts +19 -0
  346. package/dist/esm/test/TestControl.d.ts.map +1 -0
  347. package/dist/esm/test/TestControl.js +30 -0
  348. package/dist/esm/test/TestControl.js.map +1 -0
  349. package/docs/README.md +189 -0
  350. package/docs/api.md +982 -0
  351. package/docs/concepts/authority.md +109 -0
  352. package/docs/concepts/cancellation.md +129 -0
  353. package/docs/concepts/lineage.md +132 -0
  354. package/docs/concepts/ownership.md +139 -0
  355. package/docs/concepts/projections.md +203 -0
  356. package/docs/concepts/receipts.md +128 -0
  357. package/docs/guides/approvals.md +284 -0
  358. package/docs/guides/cancel-and-resume.md +162 -0
  359. package/docs/guides/durable-storage.md +147 -0
  360. package/docs/guides/implement-an-executor.md +173 -0
  361. package/docs/guides/ingest-a-webhook.md +177 -0
  362. package/docs/guides/list-runs.md +160 -0
  363. package/docs/guides/monitor-runs.md +176 -0
  364. package/docs/guides/observe-health.md +147 -0
  365. package/docs/guides/postgres-tests.md +7 -0
  366. package/docs/guides/serve-over-rpc.md +220 -0
  367. package/docs/guides/signal-a-run.md +53 -0
  368. package/docs/guides/steer-a-run.md +138 -0
  369. package/docs/guides/store-credentials.md +164 -0
  370. package/docs/guides/testing.md +139 -0
  371. package/docs/guides/watch-a-run.md +154 -0
  372. package/docs/installation.md +106 -0
  373. package/docs/quickstart.md +163 -0
  374. package/docs/troubleshooting.md +208 -0
  375. package/package.json +405 -3
  376. package/src/ApprovalAuthority.ts +114 -0
  377. package/src/Cancellation.ts +172 -0
  378. package/src/Channels.ts +493 -0
  379. package/src/Control.ts +337 -0
  380. package/src/ControlClient.ts +319 -0
  381. package/src/ControlError.ts +378 -0
  382. package/src/ControlExecutor.ts +490 -0
  383. package/src/ControlFacts.ts +383 -0
  384. package/src/ControlLive.ts +2127 -0
  385. package/src/ControlRpcs.ts +443 -0
  386. package/src/ControlRuntime.ts +1601 -0
  387. package/src/ControlSchema.ts +1380 -0
  388. package/src/ControlServer.ts +182 -0
  389. package/src/Credential.ts +310 -0
  390. package/src/CredentialCipher.ts +110 -0
  391. package/src/CredentialStore.ts +152 -0
  392. package/src/DispatchReader.ts +122 -0
  393. package/src/Health.ts +591 -0
  394. package/src/JevSessionChecker.ts +127 -0
  395. package/src/Lineage.ts +203 -0
  396. package/src/Migrations.ts +56 -0
  397. package/src/Monitor.ts +600 -0
  398. package/src/ScopedToken.ts +306 -0
  399. package/src/SqlControlRuntime.ts +2478 -0
  400. package/src/SqlCredentialStore.ts +148 -0
  401. package/src/Steering.ts +96 -0
  402. package/src/SystemFlows.ts +225 -0
  403. package/src/WebCryptoCipher.ts +169 -0
  404. package/src/WebhookChannel.ts +166 -0
  405. package/src/index.ts +188 -0
  406. package/src/internal/MutationBoundary.ts +46 -0
  407. package/src/internal/activeFibers.ts +22 -0
  408. package/src/internal/issues.ts +40 -0
  409. package/src/internal/planning.ts +262 -0
  410. package/src/internal/sqlSchemaErrors.ts +37 -0
  411. package/src/migrations/0001_control_tables.ts +99 -0
  412. package/src/migrations/0002_run_keys.ts +23 -0
  413. package/src/migrations/0003_signal_commands.ts +27 -0
  414. package/src/migrations/0004_approval_decisions.ts +25 -0
  415. package/src/migrations/0005_signal_principals.ts +27 -0
  416. package/src/migrations/0006_run_principals.ts +31 -0
  417. package/src/migrations/0007_resume_consent.ts +28 -0
  418. package/src/test/TestControl.ts +47 -0
@@ -0,0 +1,1756 @@
1
+ /**
2
+ * Durable `ControlRuntime` over `@smthrs/journal`'s fenced `RunStore` and a
3
+ * SQL database.
4
+ *
5
+ * `layerMemory` models the production seams but keeps everything in a `Map`, so
6
+ * nothing it decides survives the process. This adapter supplies the durable
7
+ * boundary the header of `ControlRuntime` describes.
8
+ *
9
+ * ## Where ownership lives
10
+ *
11
+ * The run lifecycle is not re-implemented here. `RunStore` already owns it, and
12
+ * it is the piece that is hard to get right: every ownership move is a single
13
+ * SQL compare-and-swap, so a stale writer loses the `UPDATE` rather than
14
+ * racing a read-then-write. This module maps the control plane's vocabulary
15
+ * onto it:
16
+ *
17
+ * | control status | `RunStore` status | ownership |
18
+ * | --- | --- | --- |
19
+ * | `accepted`, `running` | `running` | held by this process |
20
+ * | `accepted` after an executor declines | `suspended` | released |
21
+ * | `parked`, `waiting-approval` | `suspended` | released |
22
+ * | `cancelled` / `completed` / `failed` | same | released, terminal |
23
+ *
24
+ * The authoritative `RunSummary` is written into the row's `state_json` by the
25
+ * same fenced `UPDATE` that moves the status, so a projection can never be read
26
+ * out of step with the lifecycle. `RunStore` has no list operation, so
27
+ * `control_runs` is kept as a plain id index and each summary is read back
28
+ * through the store rather than duplicated.
29
+ *
30
+ * ## Fences
31
+ *
32
+ * A fence is a serialized `OwnerId`. `hostId` and `pid` identify the process;
33
+ * the `nonce` is regenerated on **every** claim, so a fence taken before a
34
+ * park is not the fence held after the resume that follows it, and the stale
35
+ * one is refused by the CAS. This is the `rangeID`-style monotonic fence from
36
+ * `reference/temporal`'s history service, narrowed to a per-run token.
37
+ *
38
+ * ## Browser safety
39
+ *
40
+ * Nothing here imports `node:*`. Identity comes from `globalThis.crypto`, and
41
+ * persistence is the driver-neutral SQL contract, so this module runs anywhere
42
+ * `@smthrs/database` has a driver. Fibers are the exception and are
43
+ * deliberately process-local: a fiber is a live continuation, not a row, and
44
+ * cancellation is interruption of the fiber that this process owns.
45
+ *
46
+ * @since 0.1.0
47
+ */
48
+ import * as Dialect from "@smthrs/database/Dialect";
49
+ import { DurableWriter } from "@smthrs/database/DurableWriter";
50
+ import * as DurableWrites from "@smthrs/database/DurableWriter";
51
+ import { Ownership, RunStore } from "@smthrs/run-store";
52
+ import { Clock, Crypto, Effect, Fiber, Layer, Option, Schema } from "effect";
53
+ import * as SqlClient from "effect/unstable/sql/SqlClient";
54
+ import * as ApprovalAuthority from "./ApprovalAuthority.js";
55
+ import * as Attribution from "./Cancellation.js";
56
+ import { AlreadyResolved, ClaimLost, EnvelopeMismatch, FlowNotFound, InvalidInput, PersistenceError, PlanDenied, PlanDigestMismatch, PlanNotFound, RunNotFound, Unauthorized } from "./ControlError.js";
57
+ import * as ControlExecutor from "./ControlExecutor.js";
58
+ import { ApprovalDecision, ControlRuntime, idPageLimit, make } from "./ControlRuntime.js";
59
+ import { ApprovalTarget, Envelope, GrantScope, PlanCard, PlanDecision, Principal, Receipt, RunSummary, SignalPayload } from "./ControlSchema.js";
60
+ import * as ActiveFibers from "./internal/activeFibers.js";
61
+ import { canonicalIssue, cappedIssue, schemaIssuePath } from "./internal/issues.js";
62
+ import { accepted, adoptedCode, alreadyApplied, budgeted, canonical, codeDriftOf, emptyEnvelope, planCard, planFingerprint, sameEnvelope } from "./internal/planning.js";
63
+ import { causeMessages, missingTable } from "./internal/sqlSchemaErrors.js";
64
+ import * as Lineage from "./Lineage.js";
65
+ import * as Migrations from "./Migrations.js";
66
+ import { plannable } from "./SystemFlows.js";
67
+ const persistence = (operation) => (cause) => new PersistenceError({
68
+ operation,
69
+ message: `Control runtime failed to ${operation}`,
70
+ cause
71
+ });
72
+ const storedFailure = (location, path, reason) => new PersistenceError({
73
+ operation: `decode ${location}`,
74
+ message: `Control runtime could not decode ${location}: ${cappedIssue(path, reason)}`
75
+ });
76
+ const decodeStoredValue = (location, schema, value) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError((error) => storedFailure(location, schemaIssuePath(error), "stored value does not match its schema")));
77
+ const decodeStoredJson = (location, schema, json) => Effect.try({
78
+ try: () => JSON.parse(json),
79
+ catch: () => storedFailure(location, "$", "stored value is not valid JSON")
80
+ }).pipe(Effect.flatMap((value) => decodeStoredValue(location, schema, value)));
81
+ /** The admitting principal a signal command recorded, spread onto the command. */
82
+ /** `summary` with the launcher its run's launch index recorded, if any. */
83
+ const withLauncher = (summary, launcher) => launcher === undefined ? summary : { ...summary, launchedBy: { id: launcher.id, kind: launcher.kind } };
84
+ const signalPrincipal = (json) => json === null ? Effect.succeed({}) : Effect.map(decodeStoredJson("control_signal_commands.principal_json", Principal, json), (principal) => ({ principal }));
85
+ /** A random identifier that does not depend on any Node API. */
86
+ const randomId = () => globalThis.crypto.randomUUID();
87
+ const terminal = (status) => status === "cancelled" || status === "completed" || status === "failed";
88
+ /**
89
+ * The deepest nesting the wait walk climbs.
90
+ *
91
+ * It matches `@smthrs/engine-store` `waitingTreeMaxDepth` and exists for the
92
+ * same reason: authored nesting is a handful of executions deep, and the cap
93
+ * bounds the read of a tree whose edges were corrupted outside the engine.
94
+ */
95
+ const maxWaitTreeDepth = 64;
96
+ /**
97
+ * Whether this database lacks the engine's wait-tree schema.
98
+ *
99
+ * Run-store installs the wait and trampoline columns before the engine
100
+ * installs spawn edges and `execution_flow`. A control-only database can
101
+ * lack the latter without making its ordinary run listings unreadable.
102
+ */
103
+ const missingWaitTreeSchema = (cause) => missingTable("flows_runs")(cause) || missingTable("flows_run_parents")(cause) ||
104
+ causeMessages(cause).some((message) => message.includes("execution_flow"));
105
+ /**
106
+ * The question a park declared, as JSON, or nothing.
107
+ *
108
+ * The column is the wait's own text under a `json_valid` check, so text that
109
+ * does not parse is text nothing wrote: it reads as a park that declared no
110
+ * question rather than failing the listing it appears in.
111
+ */
112
+ const declaredRequest = (value) => {
113
+ if (value === null)
114
+ return {};
115
+ try {
116
+ return { request: JSON.parse(value) };
117
+ }
118
+ catch {
119
+ return {};
120
+ }
121
+ };
122
+ /** The `RunStore` status the control plane's status projects onto. */
123
+ const storeStatus = (status) => {
124
+ switch (status) {
125
+ case "accepted":
126
+ case "running":
127
+ return "running";
128
+ case "parked":
129
+ case "waiting-approval":
130
+ return "suspended";
131
+ default:
132
+ return status;
133
+ }
134
+ };
135
+ /**
136
+ * Whether two identities are the same *process*.
137
+ *
138
+ * The nonce is deliberately excluded: it is the per-claim fence, so a process
139
+ * that re-claims a run has a new nonce but is still the same owner.
140
+ */
141
+ const sameProcess = (left, right) => left.hostId === right.hostId && left.pid === right.pid;
142
+ const approvalIdentity = (target) => target._tag === "Plan"
143
+ ? { targetTag: target._tag, runId: "", targetId: target.planId }
144
+ : { targetTag: target._tag, runId: target.runId, targetId: target.requestId };
145
+ const sameApprovalIdentity = (left, right) => left._tag === "Plan"
146
+ ? right._tag === "Plan" && left.planId === right.planId
147
+ : right._tag === "Node" && left.runId === right.runId && left.requestId === right.requestId;
148
+ const tokenFromRow = (row, target) => Effect.gen(function* () {
149
+ const decision = row.decisionJson === null
150
+ ? { _tag: "Pending" }
151
+ : yield* decodeStoredJson("control_tokens.decision_json", ApprovalDecision, row.decisionJson);
152
+ if (row.decisionJson === null && row.resolved === 1) {
153
+ return yield* new PersistenceError({
154
+ operation: "recover an approval decision",
155
+ message: "This legacy approval erased its decision. Preserve the database for review; create a new run and request a new approval. It cannot safely resume through this gate."
156
+ });
157
+ }
158
+ const expectedPrincipal = decision._tag === "Pending" ? undefined : decision.decisionPrincipal;
159
+ const storedPrincipal = row.decisionPrincipalJson === null
160
+ ? undefined
161
+ : yield* decodeStoredJson("control_tokens.decision_principal_json", Principal, row.decisionPrincipalJson);
162
+ if (row.resolved !== (decision._tag === "Pending" ? 0 : 1) ||
163
+ JSON.stringify(storedPrincipal) !== JSON.stringify(expectedPrincipal)) {
164
+ return yield* storedFailure("control_tokens.decision_json", "$", "approval decision disagrees with its resolution");
165
+ }
166
+ return {
167
+ tokenId: row.tokenId,
168
+ target,
169
+ ...decision
170
+ };
171
+ });
172
+ /** Plans read per statement while a filtered plan page fills. */
173
+ const planScanBatch = 200;
174
+ const CancelRequestPayload = Schema.Struct({
175
+ principal: Schema.optional(Principal),
176
+ reason: Schema.optional(Schema.String)
177
+ });
178
+ const EngineInterruptionPayload = Schema.Struct({
179
+ outcome: Schema.optional(Schema.String),
180
+ interruptedAtMs: Schema.optional(Schema.Number)
181
+ });
182
+ // Control only projects an engine-owned row's flow name. Importing the full
183
+ // engine state schema would add an engine-store runtime dependency, so this
184
+ // validates exactly the field this package reads and permits the remaining
185
+ // engine-owned fields to pass through the struct decoder unused.
186
+ const EngineStateProjection = Schema.Struct({ flowName: Schema.NonEmptyString });
187
+ /**
188
+ * Creates every control-plane table.
189
+ *
190
+ * `RunStore`'s own migrations are the journal package's business and are
191
+ * applied by its layer. A read-only client installs nothing.
192
+ *
193
+ * @category migrations
194
+ * @since 0.1.0
195
+ */
196
+ export const migrate = Effect.gen(function* () {
197
+ const sql = yield* SqlClient.SqlClient;
198
+ // A read-only observer reads the schema it finds and installs nothing.
199
+ if (Dialect.isReadOnly(sql))
200
+ return;
201
+ // A standalone runtime cannot record control's high-offset migration first:
202
+ // that high-water mark would make later journal and run-store sets look
203
+ // skipped. The idempotent bootstrap keeps standalone construction safe. The
204
+ // cross-package follow-up is for the host to compose `Migrations.set` beside
205
+ // the journal and run-store sets before it constructs any adapter.
206
+ // Reuse the canonical migration set and the shared transaction/retry policy,
207
+ // without advancing the shared migration ledger during standalone bootstrap.
208
+ yield* DurableWrites.make(sql).write(Effect.forEach(Object.values(Migrations.set.migrations), (migration) => migration, { discard: true }));
209
+ }).pipe(Effect.mapError(persistence("migrate")));
210
+ /**
211
+ * Constructs a durable runtime over the ambient database and run store.
212
+ *
213
+ * Not exported under this name: `make` below is the single public constructor.
214
+ * Exporting both put two names for one function on the package's public
215
+ * surface, and only one of them was documented.
216
+ */
217
+ const makeRuntime = (options = {}) => {
218
+ // Snapshot before the Effect starts: construction options are caller-owned
219
+ // and may be mutated while migrations or service acquisition are suspended.
220
+ const owner = options.owner === undefined
221
+ ? Object.freeze({
222
+ hostId: `control-runtime-${randomId()}`,
223
+ pid: 1,
224
+ nonce: randomId()
225
+ })
226
+ : Object.freeze({ ...options.owner });
227
+ const isAlive = options.isAlive;
228
+ const approvalAuthority = options.approvalAuthority ?? ApprovalAuthority.local;
229
+ const authorizeApproval = approvalAuthority.authorize.bind(approvalAuthority);
230
+ return Effect.gen(function* () {
231
+ const crypto = yield* Crypto.Crypto;
232
+ const writer = yield* DurableWriter;
233
+ const runStore = yield* RunStore.RunStore;
234
+ yield* migrate;
235
+ const sql = yield* Effect.service(SqlClient.SqlClient);
236
+ const configuredFlows = options.flows ?? plannable.map((entry) => ({
237
+ flowId: entry.flowId,
238
+ description: `Reserved ${entry.verb} system flow`,
239
+ deployClass: entry.deployClass,
240
+ envelope: emptyEnvelope
241
+ }));
242
+ const readFlows = (options.loadFlows === undefined
243
+ ? Effect.succeed(configuredFlows)
244
+ : Effect.suspend(options.loadFlows)).pipe(Effect.map((entries) => new Map(entries.map((flow) => [flow.flowId, flow]))));
245
+ const readCurrentFlows = options.currentFlows === undefined
246
+ ? readFlows
247
+ : Effect.suspend(options.currentFlows).pipe(Effect.map((entries) => new Map(entries.map((flow) => [flow.flowId, flow]))));
248
+ const readAdoptedFlow = (flowId, runId) => options.adoptFlow === undefined
249
+ ? readCurrentFlows.pipe(Effect.map((flows) => flows.get(flowId)))
250
+ : Effect.suspend(() => options.adoptFlow(flowId, runId));
251
+ // Fibers are live continuations, not rows. A restarted process legitimately
252
+ // has none, and interrupting a run it does not own is the other process's
253
+ // job — so this map is process-local by design, not by omission.
254
+ const fibers = new Map();
255
+ let pendingSignalCursor = 0;
256
+ const now = Clock.currentTimeMillis;
257
+ const query = (operation) => (effect) => effect.pipe(Effect.mapError(persistence(operation)));
258
+ /**
259
+ * Runs a read that may fail on a missing table or column, and is caught.
260
+ *
261
+ * Inside an enclosing transaction the read takes a savepoint, because a
262
+ * failed statement aborts a PostgreSQL transaction and the catch could not
263
+ * recover it. Outside one it runs bare: on SQLite a top-level transaction
264
+ * is `BEGIN IMMEDIATE`, which takes the write lock for a read and fails a
265
+ * contender with `SQLITE_BUSY` while another plane holds the claim.
266
+ */
267
+ const probe = (effect) => Effect.flatMap(Effect.serviceOption(sql.transactionService), (enclosing) => Option.isSome(enclosing) ? sql.withTransaction(effect) : effect);
268
+ /** Allocates the next value of a durable counter inside one transaction. */
269
+ const nextSequence = (name) => writer.write(Effect.gen(function* () {
270
+ yield* sql `INSERT INTO control_sequences (name, value) VALUES (${name}, 0) ON CONFLICT (name) DO NOTHING`;
271
+ const rows = yield* sql `
272
+ UPDATE control_sequences SET value = value + 1
273
+ WHERE name = ${name} AND value < ${Number.MAX_SAFE_INTEGER} RETURNING value
274
+ `;
275
+ const value = Number(rows[0]?.value);
276
+ if (!Number.isSafeInteger(value) || value < 1) {
277
+ return yield* Effect.fail(new Error(`Sequence ${name} did not return a positive safe integer`));
278
+ }
279
+ return value;
280
+ })).pipe(Effect.mapError(persistence("allocate a sequence")));
281
+ const readPlan = (planId) => sql `
282
+ SELECT plan_id AS "planId", card_json AS "cardJson",
283
+ decoded_input_json AS "decodedInputJson", decision
284
+ FROM control_plans WHERE plan_id = ${planId}
285
+ `.pipe(query("read a plan"), Effect.map((rows) => Option.fromNullishOr(rows[0])));
286
+ const storedPlan = (row) => Effect.all({
287
+ card: decodeStoredJson("control_plans.card_json", PlanCard, row.cardJson),
288
+ decodedInput: decodeStoredJson("control_plans.decoded_input_json", Schema.Json, row.decodedInputJson),
289
+ decision: decodeStoredValue("control_plans.decision", PlanDecision, row.decision)
290
+ });
291
+ const requirePlan = (planId) => Effect.flatMap(readPlan(planId), Option.match({
292
+ onNone: () => Effect.fail(new PlanNotFound({ planId })),
293
+ onSome: Effect.succeed
294
+ }));
295
+ /**
296
+ * Reads a run row, translating a missing row into `RunNotFound` and every
297
+ * other store failure into `PersistenceError` — never a defect.
298
+ */
299
+ const requireRow = (runId) => runStore.get(runId).pipe(Effect.mapError((error) => error.code === "not_found_row"
300
+ ? new RunNotFound({ runId })
301
+ : persistence("read a run")(error)));
302
+ /**
303
+ * The control status a store status projects back onto.
304
+ *
305
+ * The forward map is lossy — `accepted` and `running` both store as
306
+ * `running` — so a run this plane did not launch is reported under the
307
+ * status the store can actually prove.
308
+ */
309
+ const controlStatus = (status) => {
310
+ switch (status) {
311
+ case "pending":
312
+ return "accepted";
313
+ case "running":
314
+ return "running";
315
+ case "suspended":
316
+ return "parked";
317
+ default:
318
+ return status;
319
+ }
320
+ };
321
+ /**
322
+ * Decodes one state row once, then validates the projection its keys name.
323
+ * A control summary and an engine state share the column but not a schema.
324
+ */
325
+ const decodeRunState = (stateJson) => Effect.gen(function* () {
326
+ const parsed = yield* decodeStoredJson("flows_runs.state_json", Schema.Json, stateJson);
327
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
328
+ const candidate = parsed;
329
+ if ("runId" in candidate || "flowId" in candidate || "status" in candidate) {
330
+ return {
331
+ _tag: "Control",
332
+ summary: yield* decodeStoredValue("flows_runs.state_json as RunSummary", RunSummary, parsed)
333
+ };
334
+ }
335
+ }
336
+ const engine = yield* decodeStoredValue("flows_runs.state_json as engine state", EngineStateProjection, parsed);
337
+ return { _tag: "Engine", flowName: engine.flowName };
338
+ });
339
+ const optional = (value) => value === null || value === undefined ? {} : { value };
340
+ /** `WHERE` material narrowing a column to a scope. */
341
+ const within = (column, scope) => sql.in(column, scope);
342
+ /**
343
+ * Projects a run row onto a control summary, ancestry included.
344
+ *
345
+ * Ancestry reaches the row from two different places, because the engine
346
+ * records two different relationships. `parent_run_id` is the trampoline
347
+ * chain — the round before this one — and it is the only ancestry a run
348
+ * row carries. A run another run SPAWNED records nothing in its own row:
349
+ * the edge lives in `flows_run_parents`, which is the subflow DAG cycle
350
+ * detection walks (`packages/smithers/flows/run-store/src/migrations/0002_lineage.ts`).
351
+ * A projection that read the column alone would report every child of
352
+ * every run as an orphan.
353
+ *
354
+ * The column wins when both exist, which is the case for round 1 of a run
355
+ * that was itself spawned: the round's nearest ancestor is the round
356
+ * before it, not the run that spawned round 0.
357
+ *
358
+ * @param row the run row
359
+ * @param ancestry the fork markers and spawn edges of the whole database
360
+ */
361
+ const baseSummary = (row, state) => state._tag === "Control"
362
+ ? state.summary
363
+ : {
364
+ runId: row.runId,
365
+ flowId: state.flowName,
366
+ status: controlStatus(row.status),
367
+ createdAt: row.createdAtMs,
368
+ updatedAt: row.finishedAtMs ?? row.startedAtMs ?? row.createdAtMs
369
+ };
370
+ const summaryFrom = (row, ancestry) => Effect.gen(function* () {
371
+ const base = withLauncher(baseSummary(row, yield* decodeRunState(row.stateJson)), yield* launcherOf(row.runId));
372
+ const parentRunId = optional(row.parentRunId).value ?? ancestry.spawnedBy.get(row.runId);
373
+ const lineageId = optional(row.lineageId).value;
374
+ const roundOrdinal = optional(row.roundOrdinal).value;
375
+ const origin = Lineage.originOf({
376
+ ...(parentRunId === undefined ? {} : { parentRunId }),
377
+ ...(roundOrdinal === undefined ? {} : { roundOrdinal }),
378
+ forked: ancestry.forked.has(row.runId)
379
+ });
380
+ const waitingReason = ancestry.waitingFor.get(row.runId);
381
+ const cancellation = ancestry.cancellations.get(row.runId);
382
+ const pendingResume = ancestry.pendingResumes.get(row.runId);
383
+ const pendingWaits = ancestry.humanWaits.get(row.runId);
384
+ return {
385
+ ...base,
386
+ // A run whose tree holds an open human wait is waiting on a human,
387
+ // however nested the row that holds it and whether or not its own row
388
+ // has flipped to parked yet: a parent awaiting a `.child()` is
389
+ // blocked on that child's question either way. Rolling the status up
390
+ // is what lets every existing `status: "waiting-approval"` filter —
391
+ // the gateway inbox, `smithers approvals list`, the diagnosis card —
392
+ // find a `HumanTask` parked on a descendant.
393
+ //
394
+ // Only ATTACHED waits reach here. A `detach` spawn outlives the run
395
+ // that started it (`@smthrs/engine-store` `RunState.onParentExit`),
396
+ // so the walk stops at one rather than telling a reader that a run
397
+ // which can proceed cannot.
398
+ ...(pendingWaits === undefined ? {} : { pendingWaits }),
399
+ ...(pendingWaits === undefined || !pendingWaits.some((wait) => wait.reason === ControlExecutor.humanWaitReason) ||
400
+ terminal(base.status)
401
+ ? {}
402
+ : { status: "waiting-approval" }),
403
+ ...(pendingResume === undefined ? {} : { pendingResume }),
404
+ ...(parentRunId === undefined ? {} : { parentRunId }),
405
+ ...(lineageId === undefined ? {} : { lineageId }),
406
+ ...(roundOrdinal === undefined ? {} : { roundOrdinal }),
407
+ ...(origin === undefined ? {} : { origin }),
408
+ ...(waitingReason === undefined ? {} : { waitingReason }),
409
+ ...(cancellation === undefined ? {} : { cancellation })
410
+ };
411
+ });
412
+ /**
413
+ * The runs a `fork-created` marker names.
414
+ *
415
+ * Time travel writes the marker on the forked child's own journal, which
416
+ * is the only evidence separating a fork from an ordinary child: both
417
+ * record `parent_run_id`. A composition whose journal is not this database
418
+ * has no journal table here at all, and the honest answer there is "no
419
+ * fork evidence" — not a failed projection — so exactly that one failure
420
+ * is folded into the empty set.
421
+ *
422
+ * Every other failure is reported. A locked database, a corrupt page, or
423
+ * a table that exists but no longer answers this question would otherwise
424
+ * report every fork in the deployment as an ordinary child, silently and
425
+ * for as long as the condition lasted.
426
+ */
427
+ const forkedRunIds = (scope) => sql `
428
+ SELECT DISTINCT run_id AS "runId" FROM flows_journal_events
429
+ WHERE event_type = ${Lineage.forkCreatedEventType} AND ${within("run_id", scope)}
430
+ `.pipe(Effect.map((rows) => new Set(rows.map((row) => row.runId))), Effect.catchIf(missingTable("flows_journal_events"), () => Effect.succeed(new Set())), Effect.mapError(persistence("read fork markers")));
431
+ /**
432
+ * The run that spawned each child, by child id.
433
+ *
434
+ * `seq` is the engine's store-global insertion order, so the FIRST edge is
435
+ * the creating parent. A diamond's later parents are edges too, and a
436
+ * summary names one ancestor, so the creating one is the one it names.
437
+ *
438
+ * Missing table, missing evidence, exactly as with the fork markers: a
439
+ * control plane over a database with no engine state in it observes runs
440
+ * that spawned nothing.
441
+ */
442
+ const spawnedBy = (scope) => sql `
443
+ SELECT child_id AS "childId", parent_id AS "parentId"
444
+ FROM flows_run_parents WHERE ${within("child_id", scope)} ORDER BY seq DESC
445
+ `.pipe(probe,
446
+ // Descending, so the lowest `seq` is written last and wins the key.
447
+ Effect.map((rows) => new Map(rows.map((row) => [row.childId, row.parentId]))), Effect.catchIf(missingTable("flows_run_parents"), () => Effect.succeed(new Map())), Effect.mapError(persistence("read spawn edges")));
448
+ /**
449
+ * What each parked run is waiting for, by run id.
450
+ *
451
+ * The engine writes `waiting_reason` on the run row when it parks a run
452
+ * (`packages/smithers/flows/engine-store/src/DurableEngineState.ts` `park`), and clears
453
+ * it on the wake. The control plane reads it and never writes it: a park
454
+ * belongs to whoever is holding the run, and the projection reports the
455
+ * hold rather than deciding it.
456
+ *
457
+ * The reason separates the parks a steer can end from the parks it
458
+ * cannot, so `Control.steer` needs it on the summary and not only in the
459
+ * engine's own store.
460
+ */
461
+ const waitingFor = (scope) => sql `
462
+ SELECT run_id AS "runId", waiting_reason AS "waitingReason"
463
+ FROM flows_runs WHERE waiting_reason IS NOT NULL AND ${within("run_id", scope)}
464
+ `.pipe(Effect.map((rows) => new Map(rows.map((row) => [row.runId, row.waitingReason]))), Effect.mapError(persistence("read waiting reasons")));
465
+ /**
466
+ * Open human waits in each run's tree, by every run that contains one.
467
+ *
468
+ * The walk goes UP, not down. Approval parks are rare and run trees are
469
+ * shallow, so start from the parked rows and climb every spawn and
470
+ * trampoline edge; starting from every run in scope and descending would
471
+ * re-walk the whole forest to find the same few rows. A wait therefore
472
+ * appears under its own execution AND under every ancestor of it, which
473
+ * is exactly what "does this run tree owe anybody an answer" asks.
474
+ *
475
+ * The scope filters the ANCESTOR, not the parked row: a listing wants the
476
+ * waits of the runs it is about to return, wherever those waits are held.
477
+ *
478
+ * Collapse paths to each wait/ancestor pair at their shortest depth so
479
+ * shared descendants and cycles cannot duplicate or reorder a wait.
480
+ * A database without the engine's wait-tree schema observes no nested
481
+ * waits rather than failing every listing.
482
+ */
483
+ const humanWaits = (scope) => sql `
484
+ WITH RECURSIVE human_waits(wait_run_id, ancestor_id, depth) AS (
485
+ SELECT run_id, run_id, 0 FROM flows_runs
486
+ WHERE waiting_reason IN (${ControlExecutor.humanWaitReason}, 'event')
487
+ AND waiting_token IS NOT NULL
488
+ AND status NOT IN ('completed', 'failed', 'cancelled')
489
+ UNION
490
+ SELECT human_waits.wait_run_id, parent.run_id, human_waits.depth + 1
491
+ FROM flows_runs step JOIN human_waits ON step.run_id = human_waits.ancestor_id
492
+ JOIN flows_runs parent ON parent.run_id IN (
493
+ SELECT parent_id FROM flows_run_parents WHERE child_id = step.run_id
494
+ UNION ALL
495
+ SELECT step.parent_run_id WHERE step.parent_run_id IS NOT NULL
496
+ )
497
+ WHERE human_waits.depth < ${maxWaitTreeDepth}
498
+ AND (parent.run_id = step.parent_run_id
499
+ OR COALESCE(${Dialect.jsonText(sql, sql `step.state_json`, "$.onParentExit")}, 'cancel') <> 'detach')
500
+ ), reachable(wait_run_id, ancestor_id, depth) AS (
501
+ SELECT wait_run_id, ancestor_id, MIN(depth) FROM human_waits GROUP BY wait_run_id, ancestor_id
502
+ )
503
+ SELECT
504
+ reachable.ancestor_id AS "ancestorId",
505
+ reachable.depth AS "depth",
506
+ parked.run_id AS "runId",
507
+ parked.execution_flow AS "flowId",
508
+ parked.waiting_reason AS "waitingReason",
509
+ parked.waiting_token AS "waitingToken",
510
+ parked.waiting_request AS "waitingRequest",
511
+ parked.created_at_ms AS "createdAtMs"
512
+ FROM reachable JOIN flows_runs parked ON parked.run_id = reachable.wait_run_id
513
+ WHERE ${within("reachable.ancestor_id", scope)}
514
+ ORDER BY reachable.ancestor_id, reachable.depth, parked.created_at_ms, parked.run_id
515
+ `.pipe(probe, Effect.map((rows) => {
516
+ const index = new Map();
517
+ for (const row of rows) {
518
+ const built = ControlExecutor.pendingWaitOf({
519
+ runId: row.runId,
520
+ reason: row.waitingReason,
521
+ token: row.waitingToken,
522
+ createdAt: row.createdAtMs,
523
+ ...(row.flowId === null ? {} : { flowId: row.flowId }),
524
+ ...declaredRequest(row.waitingRequest)
525
+ });
526
+ if (built === undefined)
527
+ continue;
528
+ const waits = index.get(row.ancestorId) ?? [];
529
+ waits.push(built);
530
+ index.set(row.ancestorId, waits);
531
+ }
532
+ return index;
533
+ }), Effect.catchIf(missingWaitTreeSchema, () => Effect.succeed(new Map())), Effect.mapError(persistence("read nested human waits")));
534
+ /**
535
+ * The attributed cancel requests this plane journaled, by run id.
536
+ *
537
+ * The FIRST entry for a run wins. A cancel is idempotent, so a repeat asks
538
+ * for something that already happened; the request that caused the
539
+ * cancellation is the one that gets to name the principal and the reason.
540
+ */
541
+ const cancelRequests = (scope) => sql `
542
+ SELECT run_id AS "runId", emitted_at_ms AS "emittedAtMs", payload_json AS "payloadJson"
543
+ FROM flows_journal_events
544
+ WHERE event_type = ${Attribution.requestedEventType} AND ${within("run_id", scope)}
545
+ ORDER BY run_id, seq
546
+ `.pipe(Effect.catchIf(missingTable("flows_journal_events"), () => Effect.succeed(new Array())), Effect.mapError(persistence("read cancel requests")), Effect.flatMap((rows) => Effect.gen(function* () {
547
+ const requests = new Map();
548
+ for (const row of rows) {
549
+ if (requests.has(row.runId))
550
+ continue;
551
+ const payload = yield* decodeStoredJson("flows_journal_events.payload_json for control.run.cancel-requested", CancelRequestPayload, row.payloadJson);
552
+ requests.set(row.runId, {
553
+ requestedAt: Number(row.emittedAtMs),
554
+ ...(payload.principal === undefined ? {} : { principal: payload.principal }),
555
+ ...(payload.reason === undefined ? {} : { reason: payload.reason })
556
+ });
557
+ }
558
+ return requests;
559
+ })));
560
+ /**
561
+ * When the engine journaled each run's interruption.
562
+ *
563
+ * The engine writes this record in the same transaction as the `cancelled`
564
+ * transition (`packages/smithers/flows/engine-store/src/internal/RunDriver.ts`), so it is
565
+ * the moment a cancellation actually took, as opposed to the moment
566
+ * somebody asked. A run cancelled by a peer process that never wrote a
567
+ * request column still has this.
568
+ */
569
+ const engineInterruptions = (scope) => sql `
570
+ SELECT run_id AS "runId", payload_json AS "payloadJson"
571
+ FROM flows_journal_events
572
+ WHERE event_type = ${Attribution.interruptedEventType} AND ${within("run_id", scope)}
573
+ `.pipe(Effect.catchIf(missingTable("flows_journal_events"), () => Effect.succeed(new Array())), Effect.mapError(persistence("read engine interruptions")), Effect.flatMap((rows) => Effect.gen(function* () {
574
+ const cancelled = new Map();
575
+ for (const row of rows) {
576
+ const payload = yield* decodeStoredJson("flows_journal_events.payload_json for flows.engine.interrupted", EngineInterruptionPayload, row.payloadJson);
577
+ if (payload.outcome !== "cancelled")
578
+ continue;
579
+ cancelled.set(row.runId, Number(payload.interruptedAtMs ?? 0));
580
+ }
581
+ return cancelled;
582
+ })));
583
+ /**
584
+ * The ancestry and cancel columns of every run row.
585
+ *
586
+ * Cascade is a fact about a run's ancestors, so the attribution cannot be
587
+ * decided a row at a time: the request that cancelled a child may be three
588
+ * rounds up the chain.
589
+ */
590
+ const cancelEvidence = (scope) => sql `
591
+ SELECT run_id AS "runId", parent_run_id AS "parentRunId",
592
+ cancel_requested_at_ms AS "cancelRequestedAtMs"
593
+ FROM flows_runs WHERE ${within("run_id", scope)}
594
+ `.pipe(Effect.mapError(persistence("read cancel evidence")));
595
+ /** Every cancelled run's attribution in the scope, folded in one pass. */
596
+ const cancellations = (scope) => Effect.map(Effect.all({
597
+ rows: cancelEvidence(scope),
598
+ requests: cancelRequests(scope),
599
+ interrupted: engineInterruptions(scope),
600
+ spawnedBy: spawnedBy(scope)
601
+ }), ({ interrupted, requests, rows, spawnedBy }) => Attribution.attribute({
602
+ runs: rows.map((row) => {
603
+ const parentRunId = row.parentRunId ?? spawnedBy.get(row.runId);
604
+ const cancelledAt = interrupted.get(row.runId);
605
+ return {
606
+ runId: row.runId,
607
+ ...(parentRunId === undefined || parentRunId === null ? {} : { parentRunId }),
608
+ ...(row.cancelRequestedAtMs === null ? {} : { cancelRequestedAt: Number(row.cancelRequestedAtMs) }),
609
+ ...(cancelledAt === undefined ? {} : { cancelledAt })
610
+ };
611
+ }),
612
+ requests
613
+ }));
614
+ /**
615
+ * The outstanding resume delegation of each run in the scope.
616
+ *
617
+ * Read from `control_run_resumes` rather than from the journal because the
618
+ * question is "what has not been taken up yet", which a log of what was
619
+ * asked cannot answer without a per-run cursor.
620
+ */
621
+ const pendingResumeIndex = (scope) => sql `
622
+ SELECT run_id AS "runId", requested_seq AS "requestedSeq"
623
+ FROM control_run_resumes WHERE ${within("run_id", scope)}
624
+ `.pipe(Effect.map((rows) => new Map(rows.map((row) => [row.runId, Number(row.requestedSeq)]))), Effect.mapError(persistence("read pending resumes")));
625
+ /** Every index a projection needs over one scope, read together. */
626
+ const ancestryIndex = (scope) => Effect.map(Effect.all({
627
+ forked: forkedRunIds(scope),
628
+ spawnedBy: spawnedBy(scope),
629
+ waitingFor: waitingFor(scope),
630
+ humanWaits: humanWaits(scope),
631
+ cancellations: cancellations(scope),
632
+ pendingResumes: pendingResumeIndex(scope)
633
+ }), (index) => index);
634
+ /**
635
+ * One run and every ancestor above it, nearest first.
636
+ *
637
+ * The trampoline chain is one recursive read over `parent_run_id`. A
638
+ * SPAWNED run records nothing in its own row, so when a chain runs out the
639
+ * spawn edge is looked up and the walk continues from there — one extra
640
+ * read per nesting level, and subflow nesting is shallow where a
641
+ * trampoline is long. The visited set makes corrupt ancestry terminate
642
+ * instead of taking the control plane down with it.
643
+ */
644
+ const ancestorChain = (runId) => Effect.gen(function* () {
645
+ const chain = [];
646
+ const visited = new Set();
647
+ let start = runId;
648
+ while (start !== undefined && !visited.has(start)) {
649
+ const rows = yield* sql `
650
+ WITH RECURSIVE ancestry(run_id, parent_run_id) AS (
651
+ SELECT run_id, parent_run_id FROM flows_runs WHERE run_id = ${start}
652
+ UNION
653
+ SELECT runs.run_id, runs.parent_run_id
654
+ FROM flows_runs runs JOIN ancestry ON runs.run_id = ancestry.parent_run_id
655
+ )
656
+ SELECT run_id AS "runId", parent_run_id AS "parentRunId" FROM ancestry
657
+ `.pipe(Effect.mapError(persistence("walk a run's ancestry")));
658
+ if (rows.length === 0) {
659
+ // No row at all: the caller's own `requireRow` reports that.
660
+ chain.push(start);
661
+ break;
662
+ }
663
+ let last;
664
+ for (const row of rows) {
665
+ if (visited.has(row.runId))
666
+ continue;
667
+ visited.add(row.runId);
668
+ chain.push(row.runId);
669
+ if (row.parentRunId === null)
670
+ last = row.runId;
671
+ }
672
+ // The chain ended at a row naming no parent. A run somebody SPAWNED
673
+ // records its parent in the edge table instead, so the walk
674
+ // continues from there.
675
+ const spawn = last === undefined ? undefined : (yield* spawnedBy([last])).get(last);
676
+ start = spawn;
677
+ }
678
+ return chain;
679
+ });
680
+ /**
681
+ * One run row as a control summary, whoever wrote the row.
682
+ *
683
+ * `flows_runs.state_json` is `@smthrs/run-store`'s column and it has two
684
+ * writers: this plane stores a `RunSummary` there, and the engine stores
685
+ * its own `RunState` (`version`, `flowName`, `payload`). Decoding the
686
+ * column as a `RunSummary` outright therefore failed
687
+ * `PersistenceError` for every engine-created run before the caller could
688
+ * reach the answer it was owed: `interrupt` reads the summary before it
689
+ * asks who owns the row, so cancelling a run the ENGINE owns reported a
690
+ * corrupt database instead of the `ClaimLost` that lets `Control.cancel`
691
+ * fall back to the durable request, and `resume` failed the same way
692
+ * before it could delegate to the owning driver.
693
+ *
694
+ * `baseSummary` already projects both shapes for listings. Sharing it here
695
+ * keeps one answer for one row: a control row decodes to the summary it
696
+ * stored, and an engine row is projected from the columns the run store
697
+ * owns. Nothing writes back through this path except a transition this
698
+ * plane's own fence authorized, which is a control row by construction.
699
+ */
700
+ const summaryOf = (row) => Effect.zipWith(decodeRunState(row.stateJson), launcherOf(row.runId), (state, launcher) => withLauncher(baseSummary(row, state), launcher));
701
+ const snapshotOf = (row) => ({
702
+ status: row.status,
703
+ owner: row.owner,
704
+ heartbeatAtMs: row.heartbeatAtMs
705
+ });
706
+ // A type predicate, not a plain boolean: a row this process owns has a
707
+ // non-null owner by construction, and the callers hand `row.owner` straight
708
+ // to the fenced transitions.
709
+ const ownedByUs = (row) => row.status === "running" && row.owner !== null && sameProcess(row.owner, owner);
710
+ /**
711
+ * Moves a run this process owns to a new control status, writing the
712
+ * projection in the same compare-and-swap. A lost fence is `ClaimLost`.
713
+ */
714
+ const transition = (runId, claim, summary, status) => Effect.gen(function* () {
715
+ const timestamp = yield* now;
716
+ const next = {
717
+ ...summary,
718
+ status,
719
+ updatedAt: timestamp,
720
+ ...(storeStatus(status) === "running" ? {} : { ownerId: undefined }),
721
+ // A park releases the owner columns, so the row itself stops saying
722
+ // which process is hosting the execution. The fence it was parked
723
+ // under is kept instead: it is what lets the host recognize its own
724
+ // park, and every other process tell that the execution belongs to
725
+ // one it cannot see (triage B-15). Any other status ends the park,
726
+ // so it ends the record with it.
727
+ parkedBy: storeStatus(status) === "suspended" ? JSON.stringify(claim) : undefined
728
+ };
729
+ const outcome = yield* runStore.transitionOwned(runId, claim, storeStatus(status), JSON.stringify(next)).pipe(Effect.mapError(persistence("transition a run")));
730
+ if (outcome._tag === "NotFound")
731
+ return yield* Effect.fail(new RunNotFound({ runId }));
732
+ if (outcome._tag !== "Transitioned")
733
+ return yield* Effect.fail(new ClaimLost({ runId }));
734
+ return next;
735
+ });
736
+ /**
737
+ * Evidence that a running row's owner is gone, from the configured
738
+ * liveness check, or `undefined` when there is no check or the owner is
739
+ * alive. A pid is evidence only on its own host; elsewhere the claim
740
+ * rests on the expired lease the run store verifies.
741
+ */
742
+ const deadOwner = (row) => Effect.gen(function* () {
743
+ if (isAlive === undefined || row.owner === null)
744
+ return undefined;
745
+ const nowMs = yield* now;
746
+ const alive = yield* isAlive(row.owner, { claimant: owner, heartbeatAtMs: row.heartbeatAtMs, nowMs });
747
+ if (alive)
748
+ return undefined;
749
+ return {
750
+ expectedOwner: row.owner,
751
+ checkedAtMs: nowMs,
752
+ kind: Ownership.sameHostIncarnation(row.owner, owner)
753
+ ? "same-host-pid-dead"
754
+ : "lease-expired"
755
+ };
756
+ });
757
+ /**
758
+ * Takes ownership of a suspended or pending run under a fresh nonce, or
759
+ * of a running one whose owner `evidence` says is gone.
760
+ */
761
+ const claim = (runId, row, evidence, adopted) => Effect.gen(function* () {
762
+ const timestamp = evidence?.checkedAtMs ?? (yield* now);
763
+ const claimant = { ...owner, nonce: randomId() };
764
+ const outcome = yield* runStore.claimAndOwn(runId, snapshotOf(row), claimant, timestamp, evidence).pipe(Effect.mapError(persistence("claim a run")));
765
+ if (outcome._tag === "NotFound")
766
+ return yield* Effect.fail(new RunNotFound({ runId }));
767
+ if (outcome._tag !== "Activated")
768
+ return yield* Effect.fail(new ClaimLost({ runId }));
769
+ const summary = yield* summaryOf(row);
770
+ return yield* transition(runId, claimant, {
771
+ ...summary,
772
+ ...adopted,
773
+ ownerId: JSON.stringify(claimant)
774
+ }, "accepted");
775
+ });
776
+ /**
777
+ * The run's summary with the code identity it started on.
778
+ *
779
+ * A row with no control summary of its own (a trampoline round, or a fork
780
+ * or child the engine wrote) records no identity, and checking it against
781
+ * nothing let it resume on any code. It inherits the identity of its
782
+ * nearest ancestor of the same flow that recorded one.
783
+ */
784
+ const recordedCode = (row) => Effect.gen(function* () {
785
+ const summary = yield* summaryOf(row);
786
+ const seen = new Set([row.runId]);
787
+ let parentId = optional(row.parentRunId).value ?? summary.parentRunId;
788
+ while (summary.executionDigest === undefined && summary.engineVersion === undefined &&
789
+ parentId !== undefined && !seen.has(parentId)) {
790
+ seen.add(parentId);
791
+ const parentRow = yield* requireRow(parentId).pipe(Effect.catchTag("/control/RunNotFound", () => Effect.succeed(undefined)));
792
+ if (parentRow === undefined)
793
+ break;
794
+ const parent = yield* summaryOf(parentRow);
795
+ if (parent.flowId !== summary.flowId)
796
+ break;
797
+ if (parent.executionDigest !== undefined || parent.engineVersion !== undefined) {
798
+ return { ...summary, executionDigest: parent.executionDigest, engineVersion: parent.engineVersion };
799
+ }
800
+ parentId = optional(parentRow.parentRunId).value ?? parent.parentRunId;
801
+ }
802
+ return summary;
803
+ });
804
+ /**
805
+ * The code identity an allowed drift records, read before the claim: a
806
+ * flow this host cannot run leaves the run where it was rather than
807
+ * accepted and then failed (#2740).
808
+ */
809
+ const adoptable = (row) => Effect.gen(function* () {
810
+ const summary = yield* summaryOf(row);
811
+ const flow = yield* readAdoptedFlow(summary.flowId, summary.runId);
812
+ const gone = flow === undefined
813
+ ? codeDriftOf(yield* recordedCode(row), undefined, options.engineVersion)
814
+ : undefined;
815
+ if (gone !== undefined)
816
+ return yield* gone;
817
+ return adoptedCode(summary, flow, options.engineVersion);
818
+ });
819
+ /** `resume`, recording the identity `adopt` answers on the claimed run when given. */
820
+ const resumeRun = (runId, scope, adopt) => Effect.gen(function* () {
821
+ const row = yield* requireRow(runId);
822
+ const summary = yield* summaryOf(row);
823
+ if (terminal(summary.status))
824
+ return summary;
825
+ // Start-or-join: owning the run already means resume is a no-op.
826
+ if (ownedByUs(row))
827
+ return summary;
828
+ // Parking clears ownership, but the detached host can still be alive.
829
+ // Public resume may take its park only after a same-host dead-pid
830
+ // probe; otherwise the caller would steal and interrupt its execution.
831
+ // The refusal names that host, so `Control.resume` hands the resume
832
+ // to it instead (#3342). Trusted host claims remain able to reclaim
833
+ // their engine's execution.
834
+ if (scope === "launched" && row.status === "suspended" && summary.parkedBy !== undefined) {
835
+ const parkedOwner = yield* decodeStoredJson("parked host", Ownership.OwnerId, summary.parkedBy);
836
+ if (!sameProcess(parkedOwner, owner)) {
837
+ const alive = isAlive === undefined || !Ownership.sameHostIncarnation(parkedOwner, owner)
838
+ ? true
839
+ : yield* isAlive(parkedOwner, { claimant: owner, heartbeatAtMs: row.heartbeatAtMs, nowMs: yield* now });
840
+ if (alive) {
841
+ return yield* new ClaimLost({
842
+ runId,
843
+ reason: "The host that parked this run is still alive or its liveness is unknown",
844
+ parkedBy: { hostId: parkedOwner.hostId, pid: parkedOwner.pid }
845
+ });
846
+ }
847
+ }
848
+ }
849
+ // Every public Control resume and steer wake uses launched scope.
850
+ // Engine-created runs keep their continuation and driver. Unrestricted
851
+ // claims are a trusted low-level capability for hosts that can drive
852
+ // the execution; node approval delegates through requestResume instead.
853
+ if (scope === "launched") {
854
+ const indexed = yield* sql `SELECT run_id FROM control_runs WHERE run_id = ${runId}`.pipe(Effect.mapError(persistence("read the launch index")));
855
+ if (indexed.length === 0)
856
+ return yield* new ClaimLost({ runId });
857
+ }
858
+ // A run owned by a live peer is theirs to drive. A run whose owner is
859
+ // gone is taken over, with the evidence the run store checks.
860
+ if (row.status === "running") {
861
+ const evidence = yield* deadOwner(row);
862
+ return evidence === undefined
863
+ ? yield* new ClaimLost({ runId })
864
+ : yield* claim(runId, row, evidence, adopt === undefined ? undefined : yield* adopt(row));
865
+ }
866
+ return yield* claim(runId, row, undefined, adopt === undefined ? undefined : yield* adopt(row));
867
+ });
868
+ // Match summaryFrom's durable fields in SQL. Only the selected ids are
869
+ // decoded or expanded into ancestry; one extra key determines continuation.
870
+ const runPageKeys = (request, includeSpawn = true, includeWaitRollup = true) => {
871
+ const filters = request.filters;
872
+ const source = sql `CASE WHEN indexed.created_seq IS NULL THEN 1 ELSE 0 END`;
873
+ const sequence = sql `COALESCE(indexed.created_seq, 0)`;
874
+ const controlState = sql `(${Dialect.jsonText(sql, sql `runs.state_json`, "$.runId")} IS NOT NULL
875
+ OR ${Dialect.jsonText(sql, sql `runs.state_json`, "$.flowId")} IS NOT NULL
876
+ OR ${Dialect.jsonText(sql, sql `runs.state_json`, "$.status")} IS NOT NULL)`;
877
+ const flowId = sql `CASE WHEN ${controlState} THEN ${Dialect.jsonText(sql, sql `runs.state_json`, "$.flowId")}
878
+ ELSE ${Dialect.jsonText(sql, sql `runs.state_json`, "$.flowName")} END`;
879
+ const ownStatus = sql `CASE WHEN ${controlState} THEN ${Dialect.jsonText(sql, sql `runs.state_json`, "$.status")}
880
+ ELSE CASE runs.status WHEN 'pending' THEN 'accepted' WHEN 'suspended' THEN 'parked' ELSE runs.status END END`;
881
+ // The listing filter has to agree with the summary the listing returns.
882
+ // `summaryFrom` rolls a run tree's open human waits up onto the root's
883
+ // status, so the SQL that decides which runs a `status:
884
+ // "waiting-approval"` page contains must roll them up too — otherwise
885
+ // the inbox filter skips exactly the rows the inbox is for, which is how
886
+ // run-3 stayed invisible while parked on `coding-clarification`.
887
+ const status = includeWaitRollup
888
+ ? sql `CASE
889
+ WHEN runs.status NOT IN ('completed', 'failed', 'cancelled')
890
+ AND runs.run_id IN (SELECT ancestorId FROM human_wait_ancestry)
891
+ THEN 'waiting-approval' ELSE ${ownStatus} END`
892
+ : ownStatus;
893
+ const storedParent = sql `CASE WHEN ${controlState} THEN ${Dialect.jsonText(sql, sql `runs.state_json`, "$.parentRunId")} END`;
894
+ const storedLineage = sql `CASE WHEN ${controlState} THEN ${Dialect.jsonText(sql, sql `runs.state_json`, "$.lineageId")} END`;
895
+ const parent = includeSpawn
896
+ ? sql `COALESCE(runs.parent_run_id,
897
+ (SELECT parent_id FROM flows_run_parents WHERE child_id = runs.run_id ORDER BY seq LIMIT 1),
898
+ ${storedParent})`
899
+ : sql `COALESCE(runs.parent_run_id, ${storedParent})`;
900
+ const lineage = sql `COALESCE(runs.lineage_id, ${storedLineage})`;
901
+ const after = request.cursor;
902
+ const conditions = [sql `1 = 1`];
903
+ if (filters?.flowId !== undefined)
904
+ conditions.push(sql `${flowId} = ${filters.flowId}`);
905
+ if (filters?.status !== undefined)
906
+ conditions.push(sql `${status} = ${filters.status}`);
907
+ if (filters?.terminal !== undefined) {
908
+ const isTerminal = sql `${status} IN ('completed', 'failed', 'cancelled')`;
909
+ conditions.push(filters.terminal ? isTerminal : sql `NOT (${isTerminal})`);
910
+ }
911
+ if (filters?.parentRunId !== undefined)
912
+ conditions.push(sql `${parent} = ${filters.parentRunId}`);
913
+ if (filters?.lineageId !== undefined)
914
+ conditions.push(sql `${lineage} = ${filters.lineageId}`);
915
+ if (filters?.since !== undefined)
916
+ conditions.push(sql `runs.created_at_ms >= ${filters.since}`);
917
+ if (filters?.until !== undefined)
918
+ conditions.push(sql `runs.created_at_ms < ${filters.until}`);
919
+ if (filters?.launchedBy !== undefined) {
920
+ conditions.push(sql `indexed.principal_id = ${filters.launchedBy.id}`);
921
+ if (filters.launchedBy.kind !== undefined) {
922
+ conditions.push(sql `indexed.principal_kind = ${filters.launchedBy.kind}`);
923
+ }
924
+ }
925
+ if (filters?.runIds !== undefined) {
926
+ // One JSON parameter, so a trigger's whole ledger never meets the bind-variable limit.
927
+ const ids = JSON.stringify([...new Set(filters.runIds)]);
928
+ conditions.push(sql.onDialectOrElse({
929
+ pg: () => sql `runs.run_id IN (SELECT jsonb_array_elements_text(${ids}::jsonb))`,
930
+ orElse: () => sql `runs.run_id IN (SELECT value FROM json_each(${ids}))`
931
+ }));
932
+ }
933
+ if (after !== undefined) {
934
+ conditions.push(request.order === "newest"
935
+ ? sql `(runs.created_at_ms, ${source}, ${sequence}, runs.run_id) <
936
+ (${after.createdAt}, ${after.source}, ${after.sequence}, ${after.runId})`
937
+ : request.order === "oldest"
938
+ ? sql `(runs.created_at_ms, ${source}, ${sequence}, runs.run_id) >
939
+ (${after.createdAt}, ${after.source}, ${after.sequence}, ${after.runId})`
940
+ : sql `(${source}, ${sequence}, runs.created_at_ms, runs.run_id) >
941
+ (${after.source}, ${after.sequence}, ${after.createdAt}, ${after.runId})`);
942
+ }
943
+ // Computed once for the whole page rather than per row: the set of runs
944
+ // with an open human wait at or below them, climbed from the few parked
945
+ // rows instead of descended from every run.
946
+ const waitAncestry = includeWaitRollup
947
+ ? sql `WITH RECURSIVE human_wait_ancestry(ancestorId, depth) AS (
948
+ SELECT run_id, 0 FROM flows_runs
949
+ WHERE waiting_reason = ${ControlExecutor.humanWaitReason}
950
+ AND status NOT IN ('completed', 'failed', 'cancelled')
951
+ UNION
952
+ SELECT parent.run_id, human_wait_ancestry.depth + 1
953
+ FROM flows_runs step JOIN human_wait_ancestry ON step.run_id = human_wait_ancestry.ancestorId
954
+ JOIN flows_runs parent ON parent.run_id IN (
955
+ SELECT parent_id FROM flows_run_parents WHERE child_id = step.run_id
956
+ UNION ALL
957
+ SELECT step.parent_run_id WHERE step.parent_run_id IS NOT NULL
958
+ )
959
+ WHERE human_wait_ancestry.depth < ${maxWaitTreeDepth}
960
+ AND (parent.run_id = step.parent_run_id
961
+ OR COALESCE(${Dialect.jsonText(sql, sql `step.state_json`, "$.onParentExit")}, 'cancel') <> 'detach')
962
+ )`
963
+ : sql.literal("");
964
+ return sql `
965
+ ${waitAncestry}
966
+ SELECT runs.run_id AS "runId", ${source} AS source, ${sequence} AS sequence,
967
+ runs.created_at_ms AS "createdAt"
968
+ FROM flows_runs AS runs LEFT JOIN control_runs AS indexed ON indexed.run_id = runs.run_id
969
+ WHERE ${sql.and(conditions)}
970
+ ORDER BY ${request.order === "newest"
971
+ ? sql `runs.created_at_ms DESC, ${source} DESC, ${sequence} DESC, runs.run_id DESC`
972
+ : request.order === "oldest"
973
+ ? sql `runs.created_at_ms, ${source}, ${sequence}, runs.run_id`
974
+ : sql `${source}, ${sequence}, runs.created_at_ms, runs.run_id`}
975
+ LIMIT ${request.limit + 1}
976
+ `.pipe(probe, Effect.catchIf((error) => includeSpawn && filters?.parentRunId !== undefined && missingTable("flows_run_parents")(error), () => runPageKeys(request, false, includeWaitRollup)),
977
+ // Without the engine's wait-tree schema, retain the ordinary page.
978
+ Effect.catchIf((error) => includeWaitRollup && missingWaitTreeSchema(error), () => runPageKeys(request, includeSpawn, false)), Effect.mapError(persistence("query runs")));
979
+ };
980
+ /**
981
+ * One inventory page of `table` by `rowid`: the insertion-ordered row key
982
+ * both dialects index (SQLite's b-tree key, PostgreSQL's identity column),
983
+ * so a page is one seek and `limit + 1` rows however large the table is.
984
+ * The first page pins `through` to the newest row, so a walk ends while
985
+ * inserts continue.
986
+ */
987
+ const pageByRowId = (request, table, column, operation) => Effect.gen(function* () {
988
+ yield* idPageLimit(request.limit);
989
+ const through = request.through ?? Number((yield* sql `
990
+ SELECT MAX(rowid) AS newest FROM ${sql.literal(table)}
991
+ `.pipe(query(operation)))[0]?.newest ?? 0);
992
+ const rows = yield* sql `
993
+ SELECT ${sql.literal(column)} AS id, rowid AS position FROM ${sql.literal(table)}
994
+ WHERE rowid > ${request.after ?? 0} AND rowid <= ${through}
995
+ ORDER BY rowid LIMIT ${request.limit + 1}
996
+ `.pipe(query(operation));
997
+ const selected = rows.slice(0, request.limit);
998
+ return rows.length > request.limit
999
+ ? { ids: selected.map((row) => row.id), next: Number(selected.at(-1).position), through }
1000
+ : { ids: selected.map((row) => row.id), through };
1001
+ });
1002
+ const pagePlanIds = (request) => pageByRowId(request, "control_plans", "plan_id", "page plans");
1003
+ /**
1004
+ * One page of stored plans by `rowid`. The decision narrows in SQL; the flow
1005
+ * is read from the stored card, so a page reads batches until it fills or
1006
+ * the table ends.
1007
+ */
1008
+ const queryPlans = (request) => Effect.gen(function* () {
1009
+ yield* idPageLimit(request.limit);
1010
+ const plans = [];
1011
+ let after = request.after ?? 0;
1012
+ while (true) {
1013
+ const conditions = [
1014
+ sql `rowid > ${after}`,
1015
+ ...(request.decision === undefined ? [] : [sql `decision = ${request.decision}`])
1016
+ ];
1017
+ const rows = yield* sql `
1018
+ SELECT plan_id AS "planId", card_json AS "cardJson",
1019
+ decoded_input_json AS "decodedInputJson", decision, rowid AS position
1020
+ FROM control_plans WHERE ${sql.and(conditions)}
1021
+ ORDER BY rowid LIMIT ${planScanBatch}
1022
+ `.pipe(query("page plans"));
1023
+ for (const [index, row] of rows.entries()) {
1024
+ after = Number(row.position);
1025
+ const plan = yield* storedPlan(row);
1026
+ if (request.flowId !== undefined && plan.card.flowId !== request.flowId)
1027
+ continue;
1028
+ plans.push(plan);
1029
+ if (plans.length === request.limit) {
1030
+ return index < rows.length - 1 || rows.length === planScanBatch ? { plans, next: after } : { plans };
1031
+ }
1032
+ }
1033
+ if (rows.length < planScanBatch)
1034
+ return { plans };
1035
+ }
1036
+ });
1037
+ /**
1038
+ * The recorded launcher of a run this plane launched. The launch index is
1039
+ * written once, so it outlives the control summary the engine replaces in
1040
+ * `flows_runs.state_json` when it takes the run over.
1041
+ */
1042
+ const launcherOf = (runId) => Effect.map(sql `
1043
+ SELECT principal_id AS id, principal_kind AS kind FROM control_runs WHERE run_id = ${runId}
1044
+ `.pipe(query("read a run launcher")), (rows) => {
1045
+ const row = rows[0];
1046
+ return row === undefined || row.id === null || row.kind === null ? undefined : { id: row.id, kind: row.kind };
1047
+ });
1048
+ const messages = (runId, kind, schema) => sql `
1049
+ SELECT payload_json AS "payloadJson" FROM control_run_messages
1050
+ WHERE run_id = ${runId} AND kind = ${kind} ORDER BY seq
1051
+ `.pipe(query("read run messages"), Effect.flatMap((rows) => Effect.forEach(rows, (row) => decodeStoredJson(`control_run_messages.payload_json as ${kind}`, schema, row.payloadJson))));
1052
+ const appendMessage = (runId, kind, payload) => Effect.gen(function* () {
1053
+ yield* requireRow(runId);
1054
+ yield* sql `
1055
+ INSERT INTO control_run_messages (run_id, kind, payload_json)
1056
+ VALUES (${runId}, ${kind}, ${JSON.stringify(payload)})
1057
+ `.pipe(Effect.mapError(persistence("append a run message")));
1058
+ });
1059
+ const service = make({
1060
+ authorizeApproval,
1061
+ plan: Effect.fn("SqlControlRuntime.plan")(function* (input) {
1062
+ const flow = (yield* readFlows).get(input.flowId);
1063
+ if (flow === undefined) {
1064
+ return yield* new FlowNotFound({ flowId: input.flowId });
1065
+ }
1066
+ const requestFingerprint = yield* Effect.try({
1067
+ try: () => planFingerprint(input),
1068
+ catch: (cause) => new InvalidInput({ issue: canonicalIssue(cause) })
1069
+ });
1070
+ if (input.idempotencyKey !== undefined) {
1071
+ const prior = yield* sql `
1072
+ SELECT fingerprint, plan_id AS "planId" FROM control_plan_keys
1073
+ WHERE idempotency_key = ${input.idempotencyKey}
1074
+ `.pipe(query("read a plan key"));
1075
+ const found = prior[0];
1076
+ if (found !== undefined) {
1077
+ if (found.fingerprint !== requestFingerprint) {
1078
+ return yield* new InvalidInput({
1079
+ issue: `idempotency key ${input.idempotencyKey} was used for another plan`
1080
+ });
1081
+ }
1082
+ const stored = yield* readPlan(found.planId);
1083
+ if (Option.isSome(stored)) {
1084
+ const decoded = yield* storedPlan(stored.value);
1085
+ return { card: decoded.card, created: false };
1086
+ }
1087
+ }
1088
+ }
1089
+ const decoded = yield* (flow.decode?.(input.input) ?? Effect.try({
1090
+ try: () => {
1091
+ canonical(input.input);
1092
+ return input.input;
1093
+ },
1094
+ catch: (cause) => new InvalidInput({ issue: canonicalIssue(cause) })
1095
+ }));
1096
+ const planId = `plan-${yield* nextSequence("plan")}`;
1097
+ const handoff = flow.plan === undefined ? undefined : yield* flow.plan(decoded, planId);
1098
+ const card = yield* planCard({
1099
+ planId,
1100
+ flowId: input.flowId,
1101
+ decodedInput: decoded,
1102
+ envelope: budgeted(flow.envelope, input.budget),
1103
+ deployClass: flow.deployClass,
1104
+ executionDigest: flow.executionDigest,
1105
+ handoff,
1106
+ idempotencyKey: input.idempotencyKey
1107
+ }).pipe(Effect.provideService(Crypto.Crypto, crypto));
1108
+ const identity = approvalIdentity(card.approval.target);
1109
+ // The key row is claimed FIRST, and the claim is a conditional insert
1110
+ // followed by a read of whoever holds it. `idempotency_key` is the
1111
+ // primary key, so a bare insert made two runtimes planning under one
1112
+ // key a race the loser lost with a constraint violation surfaced as
1113
+ // `PersistenceError`, instead of the winner's card the key promises.
1114
+ // Under Control.plan this write joins the journal transaction, so the
1115
+ // card, key, token and creation entry commit or roll back together.
1116
+ const outcome = yield* writer.write(Effect.gen(function* () {
1117
+ if (input.idempotencyKey !== undefined) {
1118
+ yield* sql `
1119
+ INSERT INTO control_plan_keys (idempotency_key, fingerprint, plan_id)
1120
+ VALUES (${input.idempotencyKey}, ${requestFingerprint}, ${planId})
1121
+ ON CONFLICT (idempotency_key) DO NOTHING
1122
+ `;
1123
+ const settled = yield* sql `
1124
+ SELECT fingerprint, plan_id AS "planId" FROM control_plan_keys
1125
+ WHERE idempotency_key = ${input.idempotencyKey}
1126
+ `;
1127
+ const holder = settled[0];
1128
+ if (holder !== undefined && holder.planId !== planId) {
1129
+ return { _tag: "raced", holder };
1130
+ }
1131
+ }
1132
+ // An explicit null input is stored as JSON text in the non-null column.
1133
+ const decodedJson = JSON.stringify(decoded ?? null);
1134
+ yield* sql `
1135
+ INSERT INTO control_plans (plan_id, card_json, decoded_input_json, decision)
1136
+ VALUES (${planId}, ${JSON.stringify(card)}, ${decodedJson}, 'pending')
1137
+ `;
1138
+ yield* sql `
1139
+ INSERT INTO control_tokens (
1140
+ target_tag, run_id, target_id, token_id, target_json, resolved, decision_principal_json
1141
+ )
1142
+ VALUES (
1143
+ ${identity.targetTag}, ${identity.runId}, ${identity.targetId}, ${planId},
1144
+ ${JSON.stringify(card.approval.target)}, 0, NULL
1145
+ )
1146
+ `;
1147
+ return { _tag: "stored" };
1148
+ })).pipe(Effect.mapError(persistence("store a plan")));
1149
+ if (outcome._tag === "raced") {
1150
+ if (outcome.holder.fingerprint !== requestFingerprint) {
1151
+ return yield* new InvalidInput({
1152
+ issue: `idempotency key ${String(input.idempotencyKey)} was used for another plan`
1153
+ });
1154
+ }
1155
+ const stored = yield* readPlan(outcome.holder.planId);
1156
+ if (Option.isNone(stored)) {
1157
+ return yield* new PersistenceError({
1158
+ operation: "read a plan",
1159
+ message: `plan key ${String(input.idempotencyKey)} names plan ${outcome.holder.planId}, which is absent`
1160
+ });
1161
+ }
1162
+ const decodedHolder = yield* storedPlan(stored.value);
1163
+ return { card: decodedHolder.card, created: false };
1164
+ }
1165
+ return { card, created: true };
1166
+ }),
1167
+ getPlan: Effect.fn("SqlControlRuntime.getPlan")((planId) => Effect.flatMap(requirePlan(planId), storedPlan)),
1168
+ pagePlanIds,
1169
+ queryPlans,
1170
+ lookupApproval: Effect.fn("SqlControlRuntime.lookupApproval")(function* (target) {
1171
+ const tokenId = target._tag === "Plan" ? target.planId : target.requestId;
1172
+ const identity = approvalIdentity(target);
1173
+ const rows = yield* sql `
1174
+ SELECT token_id AS "tokenId", target_json AS "targetJson", resolved,
1175
+ decision_principal_json AS "decisionPrincipalJson", decision_json AS "decisionJson"
1176
+ FROM control_tokens
1177
+ WHERE target_tag = ${identity.targetTag}
1178
+ AND run_id = ${identity.runId}
1179
+ AND target_id = ${identity.targetId}
1180
+ `.pipe(query("read an approval token"));
1181
+ const row = rows[0];
1182
+ if (row === undefined) {
1183
+ return yield* (target._tag === "Node"
1184
+ ? new RunNotFound({ runId: target.runId })
1185
+ : new PlanNotFound({ planId: target.planId }));
1186
+ }
1187
+ const stored = yield* decodeStoredJson("control_tokens.target_json", ApprovalTarget, row.targetJson);
1188
+ // The composite columns select the requested identity. The decoded copy
1189
+ // must agree too, or a rewritten JSON blob could smuggle a foreign
1190
+ // target back into the token after lookup.
1191
+ if (!sameApprovalIdentity(stored, target)) {
1192
+ return yield* new PersistenceError({
1193
+ operation: "validate an approval token",
1194
+ message: "The stored approval target does not match its identity"
1195
+ });
1196
+ }
1197
+ if (stored.digest !== target.digest) {
1198
+ return yield* new PlanDigestMismatch({
1199
+ planId: tokenId,
1200
+ expected: stored.digest,
1201
+ actual: target.digest
1202
+ });
1203
+ }
1204
+ if (!sameEnvelope(stored.envelope, target.envelope)) {
1205
+ return yield* new EnvelopeMismatch({
1206
+ planId: tokenId,
1207
+ expected: canonical(stored.envelope),
1208
+ actual: canonical(target.envelope)
1209
+ });
1210
+ }
1211
+ const token = yield* tokenFromRow(row, stored);
1212
+ if (token._tag !== "Pending")
1213
+ return yield* new AlreadyResolved({ requestId: tokenId });
1214
+ return token;
1215
+ }),
1216
+ registerApproval: Effect.fn("SqlControlRuntime.registerApproval")(function* (target) {
1217
+ yield* requireRow(target.runId);
1218
+ const identity = approvalIdentity(target);
1219
+ yield* sql `
1220
+ INSERT INTO control_tokens (
1221
+ target_tag, run_id, target_id, token_id, target_json, resolved, decision_principal_json
1222
+ )
1223
+ VALUES (
1224
+ ${identity.targetTag}, ${identity.runId}, ${identity.targetId}, ${target.requestId},
1225
+ ${JSON.stringify(target)}, 0, NULL
1226
+ )
1227
+ ON CONFLICT (target_tag, run_id, target_id) DO NOTHING
1228
+ `.pipe(Effect.mapError(persistence("register an approval token")));
1229
+ const rows = yield* sql `
1230
+ SELECT token_id AS "tokenId", target_json AS "targetJson", resolved,
1231
+ decision_principal_json AS "decisionPrincipalJson", decision_json AS "decisionJson"
1232
+ FROM control_tokens
1233
+ WHERE target_tag = ${identity.targetTag}
1234
+ AND run_id = ${identity.runId}
1235
+ AND target_id = ${identity.targetId}
1236
+ `.pipe(query("read an approval token"));
1237
+ const row = rows[0];
1238
+ if (row === undefined) {
1239
+ return yield* Effect.fail(new PersistenceError({
1240
+ operation: "register an approval token",
1241
+ message: "A registered approval token could not be read back"
1242
+ }));
1243
+ }
1244
+ const stored = yield* decodeStoredJson("control_tokens.target_json", ApprovalTarget, row.targetJson);
1245
+ if (!sameApprovalIdentity(stored, target)) {
1246
+ return yield* new PersistenceError({
1247
+ operation: "validate an approval token",
1248
+ message: "The stored approval target does not match its identity"
1249
+ });
1250
+ }
1251
+ if (stored.digest !== target.digest) {
1252
+ return yield* new PlanDigestMismatch({
1253
+ planId: target.requestId,
1254
+ expected: stored.digest,
1255
+ actual: target.digest
1256
+ });
1257
+ }
1258
+ if (!sameEnvelope(stored.envelope, target.envelope)) {
1259
+ return yield* new EnvelopeMismatch({
1260
+ planId: target.requestId,
1261
+ expected: canonical(stored.envelope),
1262
+ actual: canonical(target.envelope)
1263
+ });
1264
+ }
1265
+ return yield* tokenFromRow(row, stored);
1266
+ }),
1267
+ installBulkGrant: Effect.fn("SqlControlRuntime.installBulkGrant")(function* (token, envelope, scope) {
1268
+ const timestamp = yield* now;
1269
+ const identity = approvalIdentity(token.target);
1270
+ // The envelope is installed whole. Splitting it into capabilities here
1271
+ // would let a partial grant exist, which is exactly what the bulk-grant
1272
+ // rule forbids.
1273
+ yield* sql `
1274
+ INSERT INTO control_grants (
1275
+ target_tag, run_id, target_id, token_id, envelope_json, scope, installed_at_ms
1276
+ )
1277
+ VALUES (
1278
+ ${identity.targetTag}, ${identity.runId}, ${identity.targetId}, ${token.tokenId},
1279
+ ${JSON.stringify(envelope)}, ${scope}, ${timestamp}
1280
+ )
1281
+ ON CONFLICT (target_tag, run_id, target_id) DO NOTHING
1282
+ `.pipe(Effect.mapError(persistence("install a grant")));
1283
+ }),
1284
+ resolveApproval: Effect.fn("SqlControlRuntime.resolveApproval")(function* (token, decision, principal, scope = "once") {
1285
+ // Caller-owned objects can change while the clock or writer yields.
1286
+ // Bind both representations of the principal, and the plan update,
1287
+ // to the same captured request rather than reading the caller again.
1288
+ const requested = yield* Effect.try({
1289
+ try: () => structuredClone({ target: token.target, principal, tokenId: token.tokenId }),
1290
+ catch: () => new PersistenceError({
1291
+ operation: "record an approval decision",
1292
+ message: "The approval request cannot be captured"
1293
+ })
1294
+ });
1295
+ const identity = approvalIdentity(requested.target);
1296
+ const requestedPrincipal = requested.principal;
1297
+ const tokenId = requested.tokenId;
1298
+ const decidedAt = yield* now;
1299
+ const answer = yield* decodeStoredValue("approval decision", ApprovalDecision, decision === "approved"
1300
+ ? { _tag: "Approved", decisionPrincipal: requestedPrincipal, decidedAt, scope }
1301
+ : { _tag: "Denied", decisionPrincipal: requestedPrincipal, decidedAt });
1302
+ // Exactly once: the guard is in the UPDATE, so two concurrent decisions
1303
+ // cannot both observe an unresolved token.
1304
+ const resolved = yield* writer.write(Effect.gen(function* () {
1305
+ yield* authorizeApproval({ principal: requestedPrincipal, target: requested.target, decision, scope });
1306
+ const rows = yield* sql `
1307
+ UPDATE control_tokens
1308
+ SET resolved = 1, decision_principal_json = ${JSON.stringify(requestedPrincipal)},
1309
+ decision_json = ${JSON.stringify(answer)}
1310
+ WHERE target_tag = ${identity.targetTag}
1311
+ AND run_id = ${identity.runId}
1312
+ AND target_id = ${identity.targetId}
1313
+ AND resolved = 0
1314
+ RETURNING token_id AS "tokenId"
1315
+ `;
1316
+ if (rows.length === 0)
1317
+ return false;
1318
+ if (identity.targetTag === "Plan") {
1319
+ yield* sql `UPDATE control_plans SET decision = ${decision} WHERE plan_id = ${identity.targetId}`;
1320
+ }
1321
+ return true;
1322
+ })).pipe(Effect.mapError((error) => error instanceof Unauthorized || error instanceof PersistenceError
1323
+ ? error
1324
+ : persistence("resolve an approval")(error)));
1325
+ if (!resolved)
1326
+ return yield* new AlreadyResolved({ requestId: tokenId });
1327
+ }),
1328
+ launch: Effect.fn("SqlControlRuntime.launch")(function* (planId, requestedDigest, envelope, principal, reservedRunId) {
1329
+ const row = yield* requirePlan(planId);
1330
+ const plan = yield* storedPlan(row);
1331
+ if (plan.card.digest !== requestedDigest) {
1332
+ return yield* new PlanDigestMismatch({
1333
+ planId,
1334
+ expected: plan.card.digest,
1335
+ actual: requestedDigest
1336
+ });
1337
+ }
1338
+ if (!sameEnvelope(plan.card.envelope, envelope)) {
1339
+ return yield* new EnvelopeMismatch({
1340
+ planId,
1341
+ expected: canonical(plan.card.envelope),
1342
+ actual: canonical(envelope)
1343
+ });
1344
+ }
1345
+ if (plan.decision === "pending") {
1346
+ const parked = {
1347
+ _tag: "Parked",
1348
+ receipt: {
1349
+ _tag: "Parked",
1350
+ receiptId: `launch:${planId}`,
1351
+ planId,
1352
+ status: "waiting-approval"
1353
+ }
1354
+ };
1355
+ return parked;
1356
+ }
1357
+ if (plan.decision !== "approved")
1358
+ return yield* new PlanDenied({ planId });
1359
+ const sequence = yield* nextSequence("run");
1360
+ const runId = reservedRunId ?? `run-${sequence}`;
1361
+ const timestamp = yield* now;
1362
+ const claimant = { ...owner, nonce: randomId() };
1363
+ const summary = {
1364
+ runId,
1365
+ flowId: plan.card.flowId,
1366
+ status: "accepted",
1367
+ planId,
1368
+ planDigest: plan.card.digest,
1369
+ ...(plan.card.executionDigest === undefined ? {} : { executionDigest: plan.card.executionDigest }),
1370
+ ...(options.engineVersion === undefined ? {} : { engineVersion: options.engineVersion }),
1371
+ ownerId: JSON.stringify(claimant),
1372
+ ...(plan.card.envelope.budget.deadline === undefined
1373
+ ? {}
1374
+ : { deadlineAt: timestamp + plan.card.envelope.budget.deadline }),
1375
+ createdAt: timestamp,
1376
+ updatedAt: timestamp
1377
+ };
1378
+ yield* runStore.create(runId, JSON.stringify(summary)).pipe(Effect.mapError(persistence("create a run")));
1379
+ yield* sql `INSERT INTO control_runs (run_id, created_seq, principal_id, principal_kind)
1380
+ VALUES (${runId}, ${sequence}, ${principal?.id ?? null}, ${principal?.kind ?? null})`.pipe(Effect.mapError(persistence("index a run")));
1381
+ const outcome = yield* runStore.claimAndOwn(runId, { status: "pending", owner: null, heartbeatAtMs: null }, claimant, timestamp).pipe(Effect.mapError(persistence("claim a new run")));
1382
+ if (outcome._tag !== "Activated")
1383
+ return yield* new ClaimLost({ runId });
1384
+ const started = {
1385
+ _tag: "Started",
1386
+ receipt: accepted(`launch:${planId}:${runId}`, runId),
1387
+ run: withLauncher(summary, principal)
1388
+ };
1389
+ return started;
1390
+ }),
1391
+ getRun: Effect.fn("SqlControlRuntime.getRun")((runId) => Effect.gen(function* () {
1392
+ const row = yield* requireRow(runId);
1393
+ return yield* summaryFrom(row, yield* ancestryIndex(yield* ancestorChain(runId)));
1394
+ })),
1395
+ /**
1396
+ * Every durable run in `flows_runs` insertion order: `control_runs`
1397
+ * indexes only the runs this plane launched, while a child, a fork, and a
1398
+ * later trampoline round are created by the engine straight into
1399
+ * `flows_runs`. One indexed seek per page, with no row decoded, so a
1400
+ * caller that walks every run holds one page of ids at a time.
1401
+ */
1402
+ pageRunIds: (request) => pageByRowId(request, "flows_runs", "run_id", "page runs"),
1403
+ queryRuns: Effect.fn("SqlControlRuntime.queryRuns")(function* (request) {
1404
+ if (!Number.isSafeInteger(request.limit) || request.limit < 1 || request.limit > 500) {
1405
+ return yield* new InvalidInput({ issue: "limit: must be an integer between 1 and 500" });
1406
+ }
1407
+ const keys = yield* runPageKeys(request);
1408
+ const selected = keys.slice(0, request.limit);
1409
+ if (selected.length === 0)
1410
+ return { items: [] };
1411
+ const chains = yield* Effect.forEach(selected, (key) => ancestorChain(key.runId));
1412
+ const ancestry = yield* ancestryIndex([...new Set(chains.flat())]);
1413
+ const summaries = yield* Effect.forEach(selected, (key) => requireRow(key.runId).pipe(Effect.flatMap((row) => Effect.map(summaryFrom(row, ancestry), Option.some)), Effect.catchTag("/control/RunNotFound", () => Effect.succeed(Option.none()))));
1414
+ return {
1415
+ items: summaries.filter(Option.isSome).map((summary) => summary.value),
1416
+ ...(keys.length > request.limit ? { nextCursor: selected[selected.length - 1] } : {})
1417
+ };
1418
+ }),
1419
+ listFlows: Effect.fn("SqlControlRuntime.listFlows")(() => Effect.map(readFlows, (flows) => Array.from(flows.values(), (flow) => ({
1420
+ flowId: flow.flowId,
1421
+ description: flow.description
1422
+ }))))(),
1423
+ deliverSignal: Effect.fn("SqlControlRuntime.deliverSignal")((runId, signal) =>
1424
+ // Durable delivery, and deliberately no resumption: a signal records a
1425
+ // fact, it does not decide who runs next.
1426
+ appendMessage(runId, "signal", signal)),
1427
+ admitSignal: Effect.fn("SqlControlRuntime.admitSignal")(function* (commandId, runId, signal, principal) {
1428
+ yield* requireRow(runId);
1429
+ const payloadJson = JSON.stringify(signal);
1430
+ const principalJson = principal === undefined ? null : JSON.stringify(principal);
1431
+ yield* writer.write(sql `INSERT INTO control_signal_commands (command_id, run_id, payload_json, principal_json)
1432
+ VALUES (${commandId}, ${runId}, ${payloadJson}, ${principalJson}) ON CONFLICT(command_id) DO NOTHING`).pipe(Effect.mapError(persistence("admit signal command")));
1433
+ }),
1434
+ signalCommand: Effect.fn("SqlControlRuntime.signalCommand")(function* (commandId) {
1435
+ const rows = yield* sql `
1436
+ SELECT command_id AS "commandId", run_id AS "runId", payload_json AS "payloadJson", wait_token AS token, state,
1437
+ principal_json AS "principalJson"
1438
+ FROM control_signal_commands WHERE command_id = ${commandId}`.pipe(query("read signal command"));
1439
+ const row = rows[0];
1440
+ if (row === undefined)
1441
+ return undefined;
1442
+ return {
1443
+ commandId: row.commandId,
1444
+ runId: row.runId,
1445
+ token: row.token,
1446
+ state: row.state,
1447
+ signal: yield* decodeStoredJson("control_signal_commands.payload_json", SignalPayload, row.payloadJson),
1448
+ ...yield* signalPrincipal(row.principalJson)
1449
+ };
1450
+ }),
1451
+ pendingSignals: Effect.gen(function* () {
1452
+ const read = (after) => sql `
1453
+ SELECT seq, command_id AS "commandId", run_id AS "runId", payload_json AS "payloadJson", wait_token AS token, state,
1454
+ principal_json AS "principalJson"
1455
+ FROM control_signal_commands WHERE state = 'pending' AND seq > ${after} ORDER BY seq LIMIT 100`.pipe(query("read pending signals"));
1456
+ let rows = yield* read(pendingSignalCursor);
1457
+ if (rows.length === 0 && pendingSignalCursor !== 0)
1458
+ rows = yield* read(0);
1459
+ pendingSignalCursor = rows.at(-1)?.seq ?? 0;
1460
+ const decoded = yield* Effect.forEach(rows, (row) => Effect.all([
1461
+ decodeStoredJson("control_signal_commands.payload_json", SignalPayload, row.payloadJson),
1462
+ signalPrincipal(row.principalJson)
1463
+ ]).pipe(Effect.map(([signal, principal]) => Option.some({
1464
+ commandId: row.commandId,
1465
+ runId: row.runId,
1466
+ token: row.token,
1467
+ state: row.state,
1468
+ signal,
1469
+ ...principal
1470
+ })), Effect.catch((error) => writer.write(sql `UPDATE control_signal_commands SET state = 'rejected' WHERE command_id = ${row.commandId} AND state = 'pending'`).pipe(Effect.mapError(persistence("quarantine malformed signal")), Effect.andThen(Effect.logWarning("Malformed admitted signal rejected", { commandId: row.commandId, error })), Effect.as(Option.none())))));
1471
+ return decoded.filter(Option.isSome).map((item) => item.value);
1472
+ }),
1473
+ bindSignal: Effect.fn("SqlControlRuntime.bindSignal")((commandId, token) => writer.write(Effect.gen(function* () {
1474
+ yield* sql `UPDATE control_signal_commands SET wait_token = ${token} WHERE command_id = ${commandId} AND wait_token IS NULL AND state = 'pending' AND NOT EXISTS (SELECT 1 FROM control_signal_commands WHERE wait_token = ${token})`;
1475
+ const rows = yield* sql `SELECT wait_token AS token FROM control_signal_commands WHERE command_id = ${commandId}`;
1476
+ if (rows[0] === undefined) {
1477
+ return yield* new PersistenceError({
1478
+ operation: "bind signal",
1479
+ message: `No pending signal command ${commandId}`
1480
+ });
1481
+ }
1482
+ return rows[0].token;
1483
+ })).pipe(Effect.mapError(persistence("bind signal")))),
1484
+ settleSignal: Effect.fn("SqlControlRuntime.settleSignal")((commandId, state) => writer.write(sql `UPDATE control_signal_commands SET state = ${state} WHERE command_id = ${commandId} AND state = 'pending'`).pipe(Effect.asVoid, Effect.mapError(persistence("settle signal")))),
1485
+ deliveredSignals: Effect.fn("SqlControlRuntime.deliveredSignals")(function* (runId) {
1486
+ yield* requireRow(runId);
1487
+ const legacy = yield* messages(runId, "signal", SignalPayload);
1488
+ const rows = yield* sql `SELECT payload_json AS "payloadJson" FROM control_signal_commands WHERE run_id = ${runId} AND state != 'rejected' ORDER BY seq`
1489
+ .pipe(query("read admitted signals"));
1490
+ return [
1491
+ ...legacy,
1492
+ ...yield* Effect.forEach(rows, (row) => decodeStoredJson("control_signal_commands.payload_json", SignalPayload, row.payloadJson))
1493
+ ];
1494
+ }),
1495
+ requestResume: Effect.fn("SqlControlRuntime.requestResume")(function* (runId, options) {
1496
+ const summary = yield* summaryOf(yield* requireRow(runId));
1497
+ // A settled run has no host left to take the delegation up: recording
1498
+ // one anyway leaves an orphaned row that `pendingResumes` filters out
1499
+ // of every poll but nothing ever clears.
1500
+ if (terminal(summary.status)) {
1501
+ return yield* new InvalidInput({
1502
+ issue: `run ${runId} is ${summary.status} and cannot take a resume`
1503
+ });
1504
+ }
1505
+ const sequence = yield* nextSequence("resume");
1506
+ const timestamp = yield* now;
1507
+ const consent = options?.consent ?? null;
1508
+ // A delegation that arrives before the host took up an operator's
1509
+ // resume keeps that consent; only a newer explicit resume replaces it.
1510
+ yield* writer.write(sql `
1511
+ INSERT INTO control_run_resumes (run_id, requested_seq, requested_at_ms, consent_seq)
1512
+ VALUES (${runId}, ${sequence}, ${timestamp}, ${consent})
1513
+ ON CONFLICT (run_id) DO UPDATE SET
1514
+ requested_seq = excluded.requested_seq,
1515
+ requested_at_ms = excluded.requested_at_ms,
1516
+ consent_seq = COALESCE(excluded.consent_seq, control_run_resumes.consent_seq)
1517
+ `).pipe(Effect.mapError(persistence("record a resume delegation")));
1518
+ return sequence;
1519
+ }),
1520
+ // Terminal runs are filtered in SQL: a delegation nobody will ever take
1521
+ // up must not keep appearing in every host's poll.
1522
+ pendingResumes: sql `
1523
+ SELECT resumes.run_id AS "runId",
1524
+ resumes.requested_seq AS "requestedSeq",
1525
+ resumes.requested_at_ms AS "requestedAtMs",
1526
+ resumes.consent_seq AS "consentSeq"
1527
+ FROM control_run_resumes AS resumes
1528
+ JOIN flows_runs AS runs ON runs.run_id = resumes.run_id
1529
+ WHERE runs.status NOT IN ('completed', 'failed', 'cancelled')
1530
+ ORDER BY resumes.requested_seq
1531
+ `.pipe(query("read pending resumes"), Effect.map((rows) => rows.map((row) => ({
1532
+ runId: row.runId,
1533
+ sequence: Number(row.requestedSeq),
1534
+ requestedAtMs: Number(row.requestedAtMs),
1535
+ ...(row.consentSeq === null ? {} : { consent: Number(row.consentSeq) })
1536
+ })))),
1537
+ clearResume: Effect.fn("SqlControlRuntime.clearResume")((runId, sequence) => writer.write(sql `
1538
+ DELETE FROM control_run_resumes WHERE run_id = ${runId} AND requested_seq = ${sequence}
1539
+ `).pipe(Effect.mapError(persistence("clear a resume delegation")), Effect.asVoid)),
1540
+ registerFiber: Effect.fn("SqlControlRuntime.registerFiber")(function* (runId, fiber) {
1541
+ yield* requireRow(runId);
1542
+ ActiveFibers.register(fibers, runId, fiber);
1543
+ }),
1544
+ interrupt: Effect.fn("SqlControlRuntime.interrupt")(function* (runId, settle = (effect) => effect) {
1545
+ const row = yield* requireRow(runId);
1546
+ const summary = yield* summaryOf(row);
1547
+ // Terminality is asked FIRST, as `resume` asks it. A settled run has
1548
+ // released its owner, so `ownedByUs` is false for every process
1549
+ // including the one that ran it, and asking ownership first answered
1550
+ // `ClaimLost` — "somebody else has it" — for a run that had simply
1551
+ // finished. Its caller has a `Terminal` receipt for exactly this.
1552
+ if (terminal(summary.status))
1553
+ return summary;
1554
+ if (!ownedByUs(row))
1555
+ return yield* new ClaimLost({ runId });
1556
+ const fiber = fibers.get(runId);
1557
+ // Cancellation is fiber interruption, not a flag anyone polls.
1558
+ if (fiber !== undefined)
1559
+ yield* Fiber.interrupt(fiber);
1560
+ if (fibers.get(runId) === fiber)
1561
+ fibers.delete(runId);
1562
+ // Only reconciliation holds the writer. Re-read after finalizers and
1563
+ // retain the original fence so cleanup cannot transfer this cancel to
1564
+ // a replacement owner or overwrite a terminal outcome.
1565
+ return yield* settle(writer.write(Effect.gen(function* () {
1566
+ const current = yield* summaryOf(yield* requireRow(runId));
1567
+ if (terminal(current.status))
1568
+ return current;
1569
+ return yield* transition(runId, row.owner, current, "cancelled");
1570
+ })).pipe(Effect.catchTag("@smthrs/database/DatabaseError", (error) => Effect.fail(persistence("settle an interrupted run")(error)))));
1571
+ }),
1572
+ codeDrift: Effect.fn("SqlControlRuntime.codeDrift")(function* (runId) {
1573
+ const summary = yield* recordedCode(yield* requireRow(runId));
1574
+ const current = (yield* readCurrentFlows).get(summary.flowId);
1575
+ const pinned = summary.executionDigest === undefined || options.pinnedFlow === undefined
1576
+ ? undefined :
1577
+ yield* options.pinnedFlow(summary.flowId, summary.executionDigest);
1578
+ return codeDriftOf(summary, pinned === true ?
1579
+ { executionDigest: summary.executionDigest }
1580
+ : pinned === false
1581
+ ? undefined
1582
+ : current, options.engineVersion);
1583
+ }),
1584
+ recordedCode: Effect.fn("SqlControlRuntime.recordedCode")(function* (runId) {
1585
+ const { executionDigest, engineVersion } = yield* recordedCode(yield* requireRow(runId));
1586
+ return { executionDigest, engineVersion };
1587
+ }),
1588
+ resume: Effect.fn("SqlControlRuntime.resume")((runId, resumeOptions) => resumeRun(runId, resumeOptions?.scope)),
1589
+ resumeAdopting: Effect.fn("SqlControlRuntime.resumeAdopting")((runId, resumeOptions) => resumeRun(runId, resumeOptions?.scope, adoptable)),
1590
+ claimFence: Effect.fn("SqlControlRuntime.claimFence")(function* (runId) {
1591
+ const row = yield* requireRow(runId);
1592
+ if (!ownedByUs(row))
1593
+ return yield* new ClaimLost({ runId });
1594
+ return JSON.stringify(row.owner);
1595
+ }),
1596
+ releasePending: Effect.fn("SqlControlRuntime.releasePending")(function* (runId, fence) {
1597
+ const row = yield* requireRow(runId);
1598
+ const presented = yield* decodeStoredJson("control fence", Ownership.OwnerId, fence).pipe(Effect.mapError(() => new ClaimLost({ runId })));
1599
+ const timestamp = yield* now;
1600
+ const next = {
1601
+ ...yield* summaryOf(row),
1602
+ status: "accepted",
1603
+ ownerId: undefined,
1604
+ parkedBy: undefined,
1605
+ updatedAt: timestamp
1606
+ };
1607
+ const outcome = yield* runStore.transitionOwned(runId, presented, "suspended", JSON.stringify(next)).pipe(Effect.mapError(persistence("release a pending run")));
1608
+ if (outcome._tag === "NotFound")
1609
+ return yield* new RunNotFound({ runId });
1610
+ if (outcome._tag !== "Transitioned")
1611
+ return yield* new ClaimLost({ runId });
1612
+ return next;
1613
+ }),
1614
+ writeStatus: Effect.fn("SqlControlRuntime.writeStatus")(function* (runId, fence, status) {
1615
+ const row = yield* requireRow(runId);
1616
+ const presented = yield* decodeStoredJson("control fence", Ownership.OwnerId, fence).pipe(Effect.mapError(() => new ClaimLost({ runId })));
1617
+ return yield* transition(runId, presented, yield* summaryOf(row), status);
1618
+ }),
1619
+ /**
1620
+ * The submitted identity wins, and only the clock is the runtime's.
1621
+ *
1622
+ * `Control.RunMutationInput` states the order: the runtime "supplies its
1623
+ * own principal when the caller names none". The submitted one is the
1624
+ * identity the server authenticated at its boundary, so a composition
1625
+ * default that overrode it would rename every remote operator to
1626
+ * whatever this process was built with.
1627
+ */
1628
+ stampPrincipal: Effect.fn("SqlControlRuntime.stampPrincipal")(function* (submitted) {
1629
+ const timestamp = yield* now;
1630
+ return {
1631
+ id: submitted?.id ?? options.principal?.id ?? "local",
1632
+ kind: submitted?.kind ?? options.principal?.kind ?? "operator",
1633
+ stampedAt: timestamp
1634
+ };
1635
+ }),
1636
+ lookupMutation: Effect.fn("SqlControlRuntime.lookupMutation")(function* (key, fingerprint) {
1637
+ const rows = yield* sql `
1638
+ SELECT fingerprint, receipt_json AS "receiptJson" FROM control_mutations WHERE mutation_key = ${key}
1639
+ `.pipe(query("read a mutation"));
1640
+ const row = rows[0];
1641
+ if (row === undefined)
1642
+ return undefined;
1643
+ if (row.fingerprint !== fingerprint) {
1644
+ return { _tag: "Conflict", message: `idempotency key ${key} was used for another mutation` };
1645
+ }
1646
+ const receipt = yield* decodeStoredJson("control_mutations.receipt_json", Receipt, row.receiptJson);
1647
+ return alreadyApplied(key, receipt);
1648
+ }),
1649
+ recordMutation: Effect.fn("SqlControlRuntime.recordMutation")((key, fingerprint, receipt) => writer.write(Effect.gen(function* () {
1650
+ yield* sql `
1651
+ INSERT INTO control_mutations (mutation_key, fingerprint, receipt_json)
1652
+ VALUES (${key}, ${fingerprint}, ${JSON.stringify(receipt)})
1653
+ ON CONFLICT (mutation_key) DO NOTHING
1654
+ `;
1655
+ const rows = yield* sql `
1656
+ SELECT fingerprint, receipt_json AS "receiptJson"
1657
+ FROM control_mutations WHERE mutation_key = ${key}
1658
+ `;
1659
+ const stored = rows[0];
1660
+ if (stored === undefined || stored.fingerprint !== fingerprint || stored.receiptJson !== JSON.stringify(receipt)) {
1661
+ return yield* Effect.fail(new PersistenceError({
1662
+ operation: "record a mutation",
1663
+ message: `Idempotency key ${key} was already settled by another mutation`
1664
+ }));
1665
+ }
1666
+ })).pipe(Effect.asVoid, Effect.mapError((cause) => cause instanceof PersistenceError ? cause : persistence("record a mutation")(cause)))),
1667
+ claimRunKey: Effect.fn("SqlControlRuntime.claimRunKey")((key, fingerprint) => {
1668
+ const claimant = randomId();
1669
+ return writer.write(Effect.gen(function* () {
1670
+ yield* sql `
1671
+ INSERT INTO control_run_keys (idempotency_key, fingerprint, claimant)
1672
+ VALUES (${key}, ${fingerprint}, ${claimant})
1673
+ ON CONFLICT (idempotency_key) DO NOTHING
1674
+ `;
1675
+ const holders = yield* sql `
1676
+ SELECT fingerprint, claimant FROM control_run_keys
1677
+ WHERE idempotency_key = ${key}
1678
+ `;
1679
+ const holder = holders[0];
1680
+ if (holder === undefined) {
1681
+ return yield* new PersistenceError({
1682
+ operation: "claim a run key",
1683
+ message: `Run key ${key} disappeared while it was being claimed`
1684
+ });
1685
+ }
1686
+ if (holder.fingerprint !== fingerprint) {
1687
+ return yield* new InvalidInput({
1688
+ issue: `idempotency key ${key} was used for another run`
1689
+ });
1690
+ }
1691
+ if (holder.claimant === claimant)
1692
+ return { _tag: "Claimed" };
1693
+ // Serialized writers make the winner's key and receipt visible in
1694
+ // one commit. Seeing its key without its receipt is therefore
1695
+ // corruption (or a claim written by an older, non-atomic build), not
1696
+ // permission to launch a second run.
1697
+ const rows = yield* sql `
1698
+ SELECT fingerprint, receipt_json AS "receiptJson"
1699
+ FROM control_mutations WHERE mutation_key = ${key}
1700
+ `;
1701
+ const record = rows[0];
1702
+ if (record === undefined || record.fingerprint !== fingerprint) {
1703
+ return yield* new PersistenceError({
1704
+ operation: "claim a run key",
1705
+ message: `Run key ${key} has no matching settled receipt`
1706
+ });
1707
+ }
1708
+ const receipt = yield* decodeStoredJson("control_mutations.receipt_json", Receipt, record.receiptJson);
1709
+ return { _tag: "Raced", receipt };
1710
+ })).pipe(Effect.mapError((cause) => cause instanceof InvalidInput || cause instanceof PersistenceError
1711
+ ? cause
1712
+ : persistence("claim a run key")(cause)));
1713
+ }),
1714
+ releaseRunKey: Effect.fn("SqlControlRuntime.releaseRunKey")((key) => writer.write(sql `
1715
+ DELETE FROM control_run_keys WHERE idempotency_key = ${key}
1716
+ `).pipe(Effect.asVoid, Effect.mapError(persistence("release a run key")))),
1717
+ grants: Effect.fn("SqlControlRuntime.grants")(() => sql `
1718
+ SELECT token_id AS "tokenId", envelope_json AS "envelopeJson",
1719
+ scope, installed_at_ms AS "installedAtMs"
1720
+ FROM control_grants ORDER BY installed_at_ms, target_tag, run_id, target_id
1721
+ `.pipe(query("list grants"), Effect.flatMap((rows) => Effect.forEach(rows, (row) => Effect.all({
1722
+ envelope: decodeStoredJson("control_grants.envelope_json", Envelope, row.envelopeJson),
1723
+ scope: decodeStoredValue("control_grants.scope", GrantScope, row.scope)
1724
+ }).pipe(Effect.map(({ envelope, scope }) => ({
1725
+ tokenId: row.tokenId,
1726
+ envelope,
1727
+ scope,
1728
+ installedAt: Number(row.installedAtMs)
1729
+ })))))))()
1730
+ });
1731
+ return service;
1732
+ });
1733
+ };
1734
+ /**
1735
+ * Provides a durable runtime over the ambient database and run store.
1736
+ *
1737
+ * @category layers
1738
+ * @since 0.1.0
1739
+ */
1740
+ export const layer = (options = {}) => Layer.effect(ControlRuntime)(makeRuntime(options));
1741
+ /**
1742
+ * Provides a durable runtime and the run store it needs over the ambient
1743
+ * database.
1744
+ *
1745
+ * @category layers
1746
+ * @since 0.1.0
1747
+ */
1748
+ export const layerWithStore = (options = {}) => layer(options).pipe(Layer.provideMerge(RunStore.layer));
1749
+ /**
1750
+ * Constructs a durable runtime over the ambient database and run store.
1751
+ *
1752
+ * @category constructors
1753
+ * @since 0.1.0
1754
+ */
1755
+ export { makeRuntime as make };
1756
+ //# sourceMappingURL=SqlControlRuntime.js.map