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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +1280 -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 +740 -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 +1521 -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 +1585 -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 +807 -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 +5 -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 +2113 -0
  385. package/src/ControlRpcs.ts +443 -0
  386. package/src/ControlRuntime.ts +1597 -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 +2476 -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,1521 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var SqlControlRuntime_exports = {};
30
+ __export(SqlControlRuntime_exports, {
31
+ layer: () => layer,
32
+ layerWithStore: () => layerWithStore,
33
+ make: () => makeRuntime,
34
+ migrate: () => migrate
35
+ });
36
+ module.exports = __toCommonJS(SqlControlRuntime_exports);
37
+ var Dialect = __toESM(require("@smthrs/database/Dialect"), 1);
38
+ var import_DurableWriter = require("@smthrs/database/DurableWriter");
39
+ var DurableWrites = __toESM(require("@smthrs/database/DurableWriter"), 1);
40
+ var import_run_store = require("@smthrs/run-store");
41
+ var import_effect = require("effect");
42
+ var SqlClient = __toESM(require("effect/unstable/sql/SqlClient"), 1);
43
+ var ApprovalAuthority = __toESM(require("./ApprovalAuthority.js"), 1);
44
+ var Attribution = __toESM(require("./Cancellation.js"), 1);
45
+ var import_ControlError = require("./ControlError.js");
46
+ var ControlExecutor = __toESM(require("./ControlExecutor.js"), 1);
47
+ var import_ControlRuntime = require("./ControlRuntime.js");
48
+ var import_ControlSchema = require("./ControlSchema.js");
49
+ var ActiveFibers = __toESM(require("./internal/activeFibers.js"), 1);
50
+ var import_issues = require("./internal/issues.js");
51
+ var import_planning = require("./internal/planning.js");
52
+ var import_sqlSchemaErrors = require("./internal/sqlSchemaErrors.js");
53
+ var Lineage = __toESM(require("./Lineage.js"), 1);
54
+ var Migrations = __toESM(require("./Migrations.js"), 1);
55
+ var import_SystemFlows = require("./SystemFlows.js");
56
+ const persistence = (operation) => (cause) => new import_ControlError.PersistenceError({
57
+ operation,
58
+ message: `Control runtime failed to ${operation}`,
59
+ cause
60
+ });
61
+ const storedFailure = (location, path, reason) => new import_ControlError.PersistenceError({
62
+ operation: `decode ${location}`,
63
+ message: `Control runtime could not decode ${location}: ${(0, import_issues.cappedIssue)(path, reason)}`
64
+ });
65
+ const decodeStoredValue = (location, schema, value) => import_effect.Schema.decodeUnknownEffect(schema)(value).pipe(
66
+ import_effect.Effect.mapError(
67
+ (error) => storedFailure(location, (0, import_issues.schemaIssuePath)(error), "stored value does not match its schema")
68
+ )
69
+ );
70
+ const decodeStoredJson = (location, schema, json) => import_effect.Effect.try({
71
+ try: () => JSON.parse(json),
72
+ catch: () => storedFailure(location, "$", "stored value is not valid JSON")
73
+ }).pipe(import_effect.Effect.flatMap((value) => decodeStoredValue(location, schema, value)));
74
+ const withLauncher = (summary, launcher) => launcher === void 0 ? summary : { ...summary, launchedBy: { id: launcher.id, kind: launcher.kind } };
75
+ const signalPrincipal = (json) => json === null ? import_effect.Effect.succeed({}) : import_effect.Effect.map(
76
+ decodeStoredJson("control_signal_commands.principal_json", import_ControlSchema.Principal, json),
77
+ (principal) => ({ principal })
78
+ );
79
+ const randomId = () => globalThis.crypto.randomUUID();
80
+ const terminal = (status) => status === "cancelled" || status === "completed" || status === "failed";
81
+ const maxWaitTreeDepth = 64;
82
+ const missingWaitTreeSchema = (cause) => (0, import_sqlSchemaErrors.missingTable)("flows_runs")(cause) || (0, import_sqlSchemaErrors.missingTable)("flows_run_parents")(cause) || (0, import_sqlSchemaErrors.causeMessages)(cause).some((message) => message.includes("execution_flow"));
83
+ const declaredRequest = (value) => {
84
+ if (value === null) return {};
85
+ try {
86
+ return { request: JSON.parse(value) };
87
+ } catch {
88
+ return {};
89
+ }
90
+ };
91
+ const storeStatus = (status) => {
92
+ switch (status) {
93
+ case "accepted":
94
+ case "running":
95
+ return "running";
96
+ case "parked":
97
+ case "waiting-approval":
98
+ return "suspended";
99
+ default:
100
+ return status;
101
+ }
102
+ };
103
+ const sameProcess = (left, right) => left.hostId === right.hostId && left.pid === right.pid;
104
+ const approvalIdentity = (target) => target._tag === "Plan" ? { targetTag: target._tag, runId: "", targetId: target.planId } : { targetTag: target._tag, runId: target.runId, targetId: target.requestId };
105
+ const sameApprovalIdentity = (left, right) => left._tag === "Plan" ? right._tag === "Plan" && left.planId === right.planId : right._tag === "Node" && left.runId === right.runId && left.requestId === right.requestId;
106
+ const tokenFromRow = (row, target) => import_effect.Effect.gen(function* () {
107
+ const decision = row.decisionJson === null ? { _tag: "Pending" } : yield* decodeStoredJson("control_tokens.decision_json", import_ControlRuntime.ApprovalDecision, row.decisionJson);
108
+ if (row.decisionJson === null && row.resolved === 1) {
109
+ return yield* new import_ControlError.PersistenceError({
110
+ operation: "recover an approval decision",
111
+ 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."
112
+ });
113
+ }
114
+ const expectedPrincipal = decision._tag === "Pending" ? void 0 : decision.decisionPrincipal;
115
+ const storedPrincipal = row.decisionPrincipalJson === null ? void 0 : yield* decodeStoredJson("control_tokens.decision_principal_json", import_ControlSchema.Principal, row.decisionPrincipalJson);
116
+ if (row.resolved !== (decision._tag === "Pending" ? 0 : 1) || JSON.stringify(storedPrincipal) !== JSON.stringify(expectedPrincipal)) {
117
+ return yield* storedFailure(
118
+ "control_tokens.decision_json",
119
+ "$",
120
+ "approval decision disagrees with its resolution"
121
+ );
122
+ }
123
+ return {
124
+ tokenId: row.tokenId,
125
+ target,
126
+ ...decision
127
+ };
128
+ });
129
+ const planScanBatch = 200;
130
+ const CancelRequestPayload = import_effect.Schema.Struct({
131
+ principal: import_effect.Schema.optional(import_ControlSchema.Principal),
132
+ reason: import_effect.Schema.optional(import_effect.Schema.String)
133
+ });
134
+ const EngineInterruptionPayload = import_effect.Schema.Struct({
135
+ outcome: import_effect.Schema.optional(import_effect.Schema.String),
136
+ interruptedAtMs: import_effect.Schema.optional(import_effect.Schema.Number)
137
+ });
138
+ const EngineStateProjection = import_effect.Schema.Struct({ flowName: import_effect.Schema.NonEmptyString });
139
+ const migrate = import_effect.Effect.gen(function* () {
140
+ const sql = yield* SqlClient.SqlClient;
141
+ if (Dialect.isReadOnly(sql)) return;
142
+ yield* DurableWrites.make(sql).write(
143
+ import_effect.Effect.forEach(Object.values(Migrations.set.migrations), (migration) => migration, { discard: true })
144
+ );
145
+ }).pipe(import_effect.Effect.mapError(persistence("migrate")));
146
+ const makeRuntime = (options = {}) => {
147
+ const owner = options.owner === void 0 ? Object.freeze({
148
+ hostId: `control-runtime-${randomId()}`,
149
+ pid: 1,
150
+ nonce: randomId()
151
+ }) : Object.freeze({ ...options.owner });
152
+ const isAlive = options.isAlive;
153
+ const approvalAuthority = options.approvalAuthority ?? ApprovalAuthority.local;
154
+ const authorizeApproval = approvalAuthority.authorize.bind(approvalAuthority);
155
+ return import_effect.Effect.gen(function* () {
156
+ const crypto = yield* import_effect.Crypto.Crypto;
157
+ const writer = yield* import_DurableWriter.DurableWriter;
158
+ const runStore = yield* import_run_store.RunStore.RunStore;
159
+ yield* migrate;
160
+ const sql = yield* import_effect.Effect.service(SqlClient.SqlClient);
161
+ const configuredFlows = options.flows ?? import_SystemFlows.plannable.map((entry) => ({
162
+ flowId: entry.flowId,
163
+ description: `Reserved ${entry.verb} system flow`,
164
+ deployClass: entry.deployClass,
165
+ envelope: import_planning.emptyEnvelope
166
+ }));
167
+ const readFlows = (options.loadFlows === void 0 ? import_effect.Effect.succeed(configuredFlows) : import_effect.Effect.suspend(options.loadFlows)).pipe(
168
+ import_effect.Effect.map((entries) => new Map(entries.map((flow) => [flow.flowId, flow])))
169
+ );
170
+ const readCurrentFlows = options.currentFlows === void 0 ? readFlows : import_effect.Effect.suspend(options.currentFlows).pipe(
171
+ import_effect.Effect.map((entries) => new Map(entries.map((flow) => [flow.flowId, flow])))
172
+ );
173
+ const readAdoptedFlow = (flowId, runId) => options.adoptFlow === void 0 ? readCurrentFlows.pipe(import_effect.Effect.map((flows) => flows.get(flowId))) : import_effect.Effect.suspend(() => options.adoptFlow(flowId, runId));
174
+ const fibers = /* @__PURE__ */ new Map();
175
+ let pendingSignalCursor = 0;
176
+ const now = import_effect.Clock.currentTimeMillis;
177
+ const query = (operation) => (effect) => effect.pipe(import_effect.Effect.mapError(persistence(operation)));
178
+ const probe = (effect) => import_effect.Effect.flatMap(
179
+ import_effect.Effect.serviceOption(sql.transactionService),
180
+ (enclosing) => import_effect.Option.isSome(enclosing) ? sql.withTransaction(effect) : effect
181
+ );
182
+ const nextSequence = (name) => writer.write(import_effect.Effect.gen(function* () {
183
+ yield* sql`INSERT INTO control_sequences (name, value) VALUES (${name}, 0) ON CONFLICT (name) DO NOTHING`;
184
+ const rows = yield* sql`
185
+ UPDATE control_sequences SET value = value + 1
186
+ WHERE name = ${name} AND value < ${Number.MAX_SAFE_INTEGER} RETURNING value
187
+ `;
188
+ const value = Number(rows[0]?.value);
189
+ if (!Number.isSafeInteger(value) || value < 1) {
190
+ return yield* import_effect.Effect.fail(new Error(`Sequence ${name} did not return a positive safe integer`));
191
+ }
192
+ return value;
193
+ })).pipe(import_effect.Effect.mapError(persistence("allocate a sequence")));
194
+ const readPlan = (planId) => sql`
195
+ SELECT plan_id AS "planId", card_json AS "cardJson",
196
+ decoded_input_json AS "decodedInputJson", decision
197
+ FROM control_plans WHERE plan_id = ${planId}
198
+ `.pipe(query("read a plan"), import_effect.Effect.map((rows) => import_effect.Option.fromNullishOr(rows[0])));
199
+ const storedPlan = (row) => import_effect.Effect.all({
200
+ card: decodeStoredJson("control_plans.card_json", import_ControlSchema.PlanCard, row.cardJson),
201
+ decodedInput: decodeStoredJson("control_plans.decoded_input_json", import_effect.Schema.Json, row.decodedInputJson),
202
+ decision: decodeStoredValue("control_plans.decision", import_ControlSchema.PlanDecision, row.decision)
203
+ });
204
+ const requirePlan = (planId) => import_effect.Effect.flatMap(
205
+ readPlan(planId),
206
+ import_effect.Option.match({
207
+ onNone: () => import_effect.Effect.fail(new import_ControlError.PlanNotFound({ planId })),
208
+ onSome: import_effect.Effect.succeed
209
+ })
210
+ );
211
+ const requireRow = (runId) => runStore.get(runId).pipe(
212
+ import_effect.Effect.mapError(
213
+ (error) => error.code === "not_found_row" ? new import_ControlError.RunNotFound({ runId }) : persistence("read a run")(error)
214
+ )
215
+ );
216
+ const controlStatus = (status) => {
217
+ switch (status) {
218
+ case "pending":
219
+ return "accepted";
220
+ case "running":
221
+ return "running";
222
+ case "suspended":
223
+ return "parked";
224
+ default:
225
+ return status;
226
+ }
227
+ };
228
+ const decodeRunState = (stateJson) => import_effect.Effect.gen(function* () {
229
+ const parsed = yield* decodeStoredJson("flows_runs.state_json", import_effect.Schema.Json, stateJson);
230
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
231
+ const candidate = parsed;
232
+ if ("runId" in candidate || "flowId" in candidate || "status" in candidate) {
233
+ return {
234
+ _tag: "Control",
235
+ summary: yield* decodeStoredValue("flows_runs.state_json as RunSummary", import_ControlSchema.RunSummary, parsed)
236
+ };
237
+ }
238
+ }
239
+ const engine = yield* decodeStoredValue(
240
+ "flows_runs.state_json as engine state",
241
+ EngineStateProjection,
242
+ parsed
243
+ );
244
+ return { _tag: "Engine", flowName: engine.flowName };
245
+ });
246
+ const optional = (value) => value === null || value === void 0 ? {} : { value };
247
+ const within = (column, scope) => sql.in(column, scope);
248
+ const baseSummary = (row, state) => state._tag === "Control" ? state.summary : {
249
+ runId: row.runId,
250
+ flowId: state.flowName,
251
+ status: controlStatus(row.status),
252
+ createdAt: row.createdAtMs,
253
+ updatedAt: row.finishedAtMs ?? row.startedAtMs ?? row.createdAtMs
254
+ };
255
+ const summaryFrom = (row, ancestry) => import_effect.Effect.gen(function* () {
256
+ const base = withLauncher(baseSummary(row, yield* decodeRunState(row.stateJson)), yield* launcherOf(row.runId));
257
+ const parentRunId = optional(row.parentRunId).value ?? ancestry.spawnedBy.get(row.runId);
258
+ const lineageId = optional(row.lineageId).value;
259
+ const roundOrdinal = optional(row.roundOrdinal).value;
260
+ const origin = Lineage.originOf({
261
+ ...parentRunId === void 0 ? {} : { parentRunId },
262
+ ...roundOrdinal === void 0 ? {} : { roundOrdinal },
263
+ forked: ancestry.forked.has(row.runId)
264
+ });
265
+ const waitingReason = ancestry.waitingFor.get(row.runId);
266
+ const cancellation = ancestry.cancellations.get(row.runId);
267
+ const pendingResume = ancestry.pendingResumes.get(row.runId);
268
+ const pendingWaits = ancestry.humanWaits.get(row.runId);
269
+ return {
270
+ ...base,
271
+ // A run whose tree holds an open human wait is waiting on a human,
272
+ // however nested the row that holds it and whether or not its own row
273
+ // has flipped to parked yet: a parent awaiting a `.child()` is
274
+ // blocked on that child's question either way. Rolling the status up
275
+ // is what lets every existing `status: "waiting-approval"` filter —
276
+ // the gateway inbox, `smithers approvals list`, the diagnosis card —
277
+ // find a `HumanTask` parked on a descendant.
278
+ //
279
+ // Only ATTACHED waits reach here. A `detach` spawn outlives the run
280
+ // that started it (`@smthrs/engine-store` `RunState.onParentExit`),
281
+ // so the walk stops at one rather than telling a reader that a run
282
+ // which can proceed cannot.
283
+ ...pendingWaits === void 0 ? {} : { pendingWaits },
284
+ ...pendingWaits === void 0 || !pendingWaits.some((wait) => wait.reason === ControlExecutor.humanWaitReason) || terminal(base.status) ? {} : { status: "waiting-approval" },
285
+ ...pendingResume === void 0 ? {} : { pendingResume },
286
+ ...parentRunId === void 0 ? {} : { parentRunId },
287
+ ...lineageId === void 0 ? {} : { lineageId },
288
+ ...roundOrdinal === void 0 ? {} : { roundOrdinal },
289
+ ...origin === void 0 ? {} : { origin },
290
+ ...waitingReason === void 0 ? {} : { waitingReason },
291
+ ...cancellation === void 0 ? {} : { cancellation }
292
+ };
293
+ });
294
+ const forkedRunIds = (scope) => sql`
295
+ SELECT DISTINCT run_id AS "runId" FROM flows_journal_events
296
+ WHERE event_type = ${Lineage.forkCreatedEventType} AND ${within("run_id", scope)}
297
+ `.pipe(
298
+ import_effect.Effect.map((rows) => new Set(rows.map((row) => row.runId))),
299
+ import_effect.Effect.catchIf(
300
+ (0, import_sqlSchemaErrors.missingTable)("flows_journal_events"),
301
+ () => import_effect.Effect.succeed(/* @__PURE__ */ new Set())
302
+ ),
303
+ import_effect.Effect.mapError(persistence("read fork markers"))
304
+ );
305
+ const spawnedBy = (scope) => sql`
306
+ SELECT child_id AS "childId", parent_id AS "parentId"
307
+ FROM flows_run_parents WHERE ${within("child_id", scope)} ORDER BY seq DESC
308
+ `.pipe(
309
+ probe,
310
+ // Descending, so the lowest `seq` is written last and wins the key.
311
+ import_effect.Effect.map((rows) => new Map(rows.map((row) => [row.childId, row.parentId]))),
312
+ import_effect.Effect.catchIf(
313
+ (0, import_sqlSchemaErrors.missingTable)("flows_run_parents"),
314
+ () => import_effect.Effect.succeed(/* @__PURE__ */ new Map())
315
+ ),
316
+ import_effect.Effect.mapError(persistence("read spawn edges"))
317
+ );
318
+ const waitingFor = (scope) => sql`
319
+ SELECT run_id AS "runId", waiting_reason AS "waitingReason"
320
+ FROM flows_runs WHERE waiting_reason IS NOT NULL AND ${within("run_id", scope)}
321
+ `.pipe(
322
+ import_effect.Effect.map((rows) => new Map(rows.map((row) => [row.runId, row.waitingReason]))),
323
+ import_effect.Effect.mapError(persistence("read waiting reasons"))
324
+ );
325
+ const humanWaits = (scope) => sql`
326
+ WITH RECURSIVE human_waits(wait_run_id, ancestor_id, depth) AS (
327
+ SELECT run_id, run_id, 0 FROM flows_runs
328
+ WHERE waiting_reason IN (${ControlExecutor.humanWaitReason}, 'event')
329
+ AND waiting_token IS NOT NULL
330
+ AND status NOT IN ('completed', 'failed', 'cancelled')
331
+ UNION
332
+ SELECT human_waits.wait_run_id, parent.run_id, human_waits.depth + 1
333
+ FROM flows_runs step JOIN human_waits ON step.run_id = human_waits.ancestor_id
334
+ JOIN flows_runs parent ON parent.run_id IN (
335
+ SELECT parent_id FROM flows_run_parents WHERE child_id = step.run_id
336
+ UNION ALL
337
+ SELECT step.parent_run_id WHERE step.parent_run_id IS NOT NULL
338
+ )
339
+ WHERE human_waits.depth < ${maxWaitTreeDepth}
340
+ AND (parent.run_id = step.parent_run_id
341
+ OR COALESCE(${Dialect.jsonText(sql, sql`step.state_json`, "$.onParentExit")}, 'cancel') <> 'detach')
342
+ ), reachable(wait_run_id, ancestor_id, depth) AS (
343
+ SELECT wait_run_id, ancestor_id, MIN(depth) FROM human_waits GROUP BY wait_run_id, ancestor_id
344
+ )
345
+ SELECT
346
+ reachable.ancestor_id AS "ancestorId",
347
+ reachable.depth AS "depth",
348
+ parked.run_id AS "runId",
349
+ parked.execution_flow AS "flowId",
350
+ parked.waiting_reason AS "waitingReason",
351
+ parked.waiting_token AS "waitingToken",
352
+ parked.waiting_request AS "waitingRequest",
353
+ parked.created_at_ms AS "createdAtMs"
354
+ FROM reachable JOIN flows_runs parked ON parked.run_id = reachable.wait_run_id
355
+ WHERE ${within("reachable.ancestor_id", scope)}
356
+ ORDER BY reachable.ancestor_id, reachable.depth, parked.created_at_ms, parked.run_id
357
+ `.pipe(
358
+ probe,
359
+ import_effect.Effect.map((rows) => {
360
+ const index = /* @__PURE__ */ new Map();
361
+ for (const row of rows) {
362
+ const built = ControlExecutor.pendingWaitOf({
363
+ runId: row.runId,
364
+ reason: row.waitingReason,
365
+ token: row.waitingToken,
366
+ createdAt: row.createdAtMs,
367
+ ...row.flowId === null ? {} : { flowId: row.flowId },
368
+ ...declaredRequest(row.waitingRequest)
369
+ });
370
+ if (built === void 0) continue;
371
+ const waits = index.get(row.ancestorId) ?? [];
372
+ waits.push(built);
373
+ index.set(row.ancestorId, waits);
374
+ }
375
+ return index;
376
+ }),
377
+ import_effect.Effect.catchIf(
378
+ missingWaitTreeSchema,
379
+ () => import_effect.Effect.succeed(/* @__PURE__ */ new Map())
380
+ ),
381
+ import_effect.Effect.mapError(persistence("read nested human waits"))
382
+ );
383
+ const cancelRequests = (scope) => sql`
384
+ SELECT run_id AS "runId", emitted_at_ms AS "emittedAtMs", payload_json AS "payloadJson"
385
+ FROM flows_journal_events
386
+ WHERE event_type = ${Attribution.requestedEventType} AND ${within("run_id", scope)}
387
+ ORDER BY run_id, seq
388
+ `.pipe(
389
+ import_effect.Effect.catchIf(
390
+ (0, import_sqlSchemaErrors.missingTable)("flows_journal_events"),
391
+ () => import_effect.Effect.succeed(
392
+ new Array()
393
+ )
394
+ ),
395
+ import_effect.Effect.mapError(persistence("read cancel requests")),
396
+ import_effect.Effect.flatMap(
397
+ (rows) => import_effect.Effect.gen(function* () {
398
+ const requests = /* @__PURE__ */ new Map();
399
+ for (const row of rows) {
400
+ if (requests.has(row.runId)) continue;
401
+ const payload = yield* decodeStoredJson(
402
+ "flows_journal_events.payload_json for control.run.cancel-requested",
403
+ CancelRequestPayload,
404
+ row.payloadJson
405
+ );
406
+ requests.set(row.runId, {
407
+ requestedAt: Number(row.emittedAtMs),
408
+ ...payload.principal === void 0 ? {} : { principal: payload.principal },
409
+ ...payload.reason === void 0 ? {} : { reason: payload.reason }
410
+ });
411
+ }
412
+ return requests;
413
+ })
414
+ )
415
+ );
416
+ const engineInterruptions = (scope) => sql`
417
+ SELECT run_id AS "runId", payload_json AS "payloadJson"
418
+ FROM flows_journal_events
419
+ WHERE event_type = ${Attribution.interruptedEventType} AND ${within("run_id", scope)}
420
+ `.pipe(
421
+ import_effect.Effect.catchIf(
422
+ (0, import_sqlSchemaErrors.missingTable)("flows_journal_events"),
423
+ () => import_effect.Effect.succeed(new Array())
424
+ ),
425
+ import_effect.Effect.mapError(persistence("read engine interruptions")),
426
+ import_effect.Effect.flatMap(
427
+ (rows) => import_effect.Effect.gen(function* () {
428
+ const cancelled = /* @__PURE__ */ new Map();
429
+ for (const row of rows) {
430
+ const payload = yield* decodeStoredJson(
431
+ "flows_journal_events.payload_json for flows.engine.interrupted",
432
+ EngineInterruptionPayload,
433
+ row.payloadJson
434
+ );
435
+ if (payload.outcome !== "cancelled") continue;
436
+ cancelled.set(row.runId, Number(payload.interruptedAtMs ?? 0));
437
+ }
438
+ return cancelled;
439
+ })
440
+ )
441
+ );
442
+ const cancelEvidence = (scope) => sql`
443
+ SELECT run_id AS "runId", parent_run_id AS "parentRunId",
444
+ cancel_requested_at_ms AS "cancelRequestedAtMs"
445
+ FROM flows_runs WHERE ${within("run_id", scope)}
446
+ `.pipe(import_effect.Effect.mapError(persistence("read cancel evidence")));
447
+ const cancellations = (scope) => import_effect.Effect.map(
448
+ import_effect.Effect.all({
449
+ rows: cancelEvidence(scope),
450
+ requests: cancelRequests(scope),
451
+ interrupted: engineInterruptions(scope),
452
+ spawnedBy: spawnedBy(scope)
453
+ }),
454
+ ({ interrupted, requests, rows, spawnedBy: spawnedBy2 }) => Attribution.attribute({
455
+ runs: rows.map((row) => {
456
+ const parentRunId = row.parentRunId ?? spawnedBy2.get(row.runId);
457
+ const cancelledAt = interrupted.get(row.runId);
458
+ return {
459
+ runId: row.runId,
460
+ ...parentRunId === void 0 || parentRunId === null ? {} : { parentRunId },
461
+ ...row.cancelRequestedAtMs === null ? {} : { cancelRequestedAt: Number(row.cancelRequestedAtMs) },
462
+ ...cancelledAt === void 0 ? {} : { cancelledAt }
463
+ };
464
+ }),
465
+ requests
466
+ })
467
+ );
468
+ const pendingResumeIndex = (scope) => sql`
469
+ SELECT run_id AS "runId", requested_seq AS "requestedSeq"
470
+ FROM control_run_resumes WHERE ${within("run_id", scope)}
471
+ `.pipe(
472
+ import_effect.Effect.map(
473
+ (rows) => new Map(rows.map((row) => [row.runId, Number(row.requestedSeq)]))
474
+ ),
475
+ import_effect.Effect.mapError(persistence("read pending resumes"))
476
+ );
477
+ const ancestryIndex = (scope) => import_effect.Effect.map(
478
+ import_effect.Effect.all({
479
+ forked: forkedRunIds(scope),
480
+ spawnedBy: spawnedBy(scope),
481
+ waitingFor: waitingFor(scope),
482
+ humanWaits: humanWaits(scope),
483
+ cancellations: cancellations(scope),
484
+ pendingResumes: pendingResumeIndex(scope)
485
+ }),
486
+ (index) => index
487
+ );
488
+ const ancestorChain = (runId) => import_effect.Effect.gen(function* () {
489
+ const chain = [];
490
+ const visited = /* @__PURE__ */ new Set();
491
+ let start = runId;
492
+ while (start !== void 0 && !visited.has(start)) {
493
+ const rows = yield* sql`
494
+ WITH RECURSIVE ancestry(run_id, parent_run_id) AS (
495
+ SELECT run_id, parent_run_id FROM flows_runs WHERE run_id = ${start}
496
+ UNION
497
+ SELECT runs.run_id, runs.parent_run_id
498
+ FROM flows_runs runs JOIN ancestry ON runs.run_id = ancestry.parent_run_id
499
+ )
500
+ SELECT run_id AS "runId", parent_run_id AS "parentRunId" FROM ancestry
501
+ `.pipe(import_effect.Effect.mapError(persistence("walk a run's ancestry")));
502
+ if (rows.length === 0) {
503
+ chain.push(start);
504
+ break;
505
+ }
506
+ let last;
507
+ for (const row of rows) {
508
+ if (visited.has(row.runId)) continue;
509
+ visited.add(row.runId);
510
+ chain.push(row.runId);
511
+ if (row.parentRunId === null) last = row.runId;
512
+ }
513
+ const spawn = last === void 0 ? void 0 : (yield* spawnedBy([last])).get(last);
514
+ start = spawn;
515
+ }
516
+ return chain;
517
+ });
518
+ const summaryOf = (row) => import_effect.Effect.zipWith(
519
+ decodeRunState(row.stateJson),
520
+ launcherOf(row.runId),
521
+ (state, launcher) => withLauncher(baseSummary(row, state), launcher)
522
+ );
523
+ const snapshotOf = (row) => ({
524
+ status: row.status,
525
+ owner: row.owner,
526
+ heartbeatAtMs: row.heartbeatAtMs
527
+ });
528
+ const ownedByUs = (row) => row.status === "running" && row.owner !== null && sameProcess(row.owner, owner);
529
+ const transition = (runId, claim2, summary, status) => import_effect.Effect.gen(function* () {
530
+ const timestamp = yield* now;
531
+ const next = {
532
+ ...summary,
533
+ status,
534
+ updatedAt: timestamp,
535
+ ...storeStatus(status) === "running" ? {} : { ownerId: void 0 },
536
+ // A park releases the owner columns, so the row itself stops saying
537
+ // which process is hosting the execution. The fence it was parked
538
+ // under is kept instead: it is what lets the host recognize its own
539
+ // park, and every other process tell that the execution belongs to
540
+ // one it cannot see (triage B-15). Any other status ends the park,
541
+ // so it ends the record with it.
542
+ parkedBy: storeStatus(status) === "suspended" ? JSON.stringify(claim2) : void 0
543
+ };
544
+ const outcome = yield* runStore.transitionOwned(
545
+ runId,
546
+ claim2,
547
+ storeStatus(status),
548
+ JSON.stringify(next)
549
+ ).pipe(import_effect.Effect.mapError(persistence("transition a run")));
550
+ if (outcome._tag === "NotFound") return yield* import_effect.Effect.fail(new import_ControlError.RunNotFound({ runId }));
551
+ if (outcome._tag !== "Transitioned") return yield* import_effect.Effect.fail(new import_ControlError.ClaimLost({ runId }));
552
+ return next;
553
+ });
554
+ const deadOwner = (row) => import_effect.Effect.gen(function* () {
555
+ if (isAlive === void 0 || row.owner === null) return void 0;
556
+ const nowMs = yield* now;
557
+ const alive = yield* isAlive(row.owner, { claimant: owner, heartbeatAtMs: row.heartbeatAtMs, nowMs });
558
+ if (alive) return void 0;
559
+ return {
560
+ expectedOwner: row.owner,
561
+ checkedAtMs: nowMs,
562
+ kind: import_run_store.Ownership.sameHostIncarnation(row.owner, owner) ? "same-host-pid-dead" : "lease-expired"
563
+ };
564
+ });
565
+ const claim = (runId, row, evidence, adopted) => import_effect.Effect.gen(function* () {
566
+ const timestamp = evidence?.checkedAtMs ?? (yield* now);
567
+ const claimant = { ...owner, nonce: randomId() };
568
+ const outcome = yield* runStore.claimAndOwn(runId, snapshotOf(row), claimant, timestamp, evidence).pipe(
569
+ import_effect.Effect.mapError(persistence("claim a run"))
570
+ );
571
+ if (outcome._tag === "NotFound") return yield* import_effect.Effect.fail(new import_ControlError.RunNotFound({ runId }));
572
+ if (outcome._tag !== "Activated") return yield* import_effect.Effect.fail(new import_ControlError.ClaimLost({ runId }));
573
+ const summary = yield* summaryOf(row);
574
+ return yield* transition(runId, claimant, {
575
+ ...summary,
576
+ ...adopted,
577
+ ownerId: JSON.stringify(claimant)
578
+ }, "accepted");
579
+ });
580
+ const recordedCode = (row) => import_effect.Effect.gen(function* () {
581
+ const summary = yield* summaryOf(row);
582
+ const seen = /* @__PURE__ */ new Set([row.runId]);
583
+ let parentId = optional(row.parentRunId).value ?? summary.parentRunId;
584
+ while (summary.executionDigest === void 0 && summary.engineVersion === void 0 && parentId !== void 0 && !seen.has(parentId)) {
585
+ seen.add(parentId);
586
+ const parentRow = yield* requireRow(parentId).pipe(
587
+ import_effect.Effect.catchTag("/control/RunNotFound", () => import_effect.Effect.succeed(void 0))
588
+ );
589
+ if (parentRow === void 0) break;
590
+ const parent = yield* summaryOf(parentRow);
591
+ if (parent.flowId !== summary.flowId) break;
592
+ if (parent.executionDigest !== void 0 || parent.engineVersion !== void 0) {
593
+ return { ...summary, executionDigest: parent.executionDigest, engineVersion: parent.engineVersion };
594
+ }
595
+ parentId = optional(parentRow.parentRunId).value ?? parent.parentRunId;
596
+ }
597
+ return summary;
598
+ });
599
+ const adoptable = (row) => import_effect.Effect.gen(function* () {
600
+ const summary = yield* summaryOf(row);
601
+ const flow = yield* readAdoptedFlow(summary.flowId, summary.runId);
602
+ const gone = flow === void 0 ? (0, import_planning.codeDriftOf)(yield* recordedCode(row), void 0, options.engineVersion) : void 0;
603
+ if (gone !== void 0) return yield* gone;
604
+ return (0, import_planning.adoptedCode)(summary, flow, options.engineVersion);
605
+ });
606
+ const resumeRun = (runId, scope, adopt) => import_effect.Effect.gen(function* () {
607
+ const row = yield* requireRow(runId);
608
+ const summary = yield* summaryOf(row);
609
+ if (terminal(summary.status)) return summary;
610
+ if (ownedByUs(row)) return summary;
611
+ if (scope === "launched" && row.status === "suspended" && summary.parkedBy !== void 0) {
612
+ const parkedOwner = yield* decodeStoredJson("parked host", import_run_store.Ownership.OwnerId, summary.parkedBy);
613
+ if (!sameProcess(parkedOwner, owner)) {
614
+ const alive = isAlive === void 0 || !import_run_store.Ownership.sameHostIncarnation(parkedOwner, owner) ? true : yield* isAlive(parkedOwner, { claimant: owner, heartbeatAtMs: row.heartbeatAtMs, nowMs: yield* now });
615
+ if (alive) {
616
+ return yield* new import_ControlError.ClaimLost({
617
+ runId,
618
+ reason: "The host that parked this run is still alive or its liveness is unknown",
619
+ parkedBy: { hostId: parkedOwner.hostId, pid: parkedOwner.pid }
620
+ });
621
+ }
622
+ }
623
+ }
624
+ if (scope === "launched") {
625
+ const indexed = yield* sql`SELECT run_id FROM control_runs WHERE run_id = ${runId}`.pipe(
626
+ import_effect.Effect.mapError(persistence("read the launch index"))
627
+ );
628
+ if (indexed.length === 0) return yield* new import_ControlError.ClaimLost({ runId });
629
+ }
630
+ if (row.status === "running") {
631
+ const evidence = yield* deadOwner(row);
632
+ return evidence === void 0 ? yield* new import_ControlError.ClaimLost({ runId }) : yield* claim(runId, row, evidence, adopt === void 0 ? void 0 : yield* adopt(row));
633
+ }
634
+ return yield* claim(runId, row, void 0, adopt === void 0 ? void 0 : yield* adopt(row));
635
+ });
636
+ const runPageKeys = (request, includeSpawn = true, includeWaitRollup = true) => {
637
+ const filters = request.filters;
638
+ const source = sql`CASE WHEN indexed.created_seq IS NULL THEN 1 ELSE 0 END`;
639
+ const sequence = sql`COALESCE(indexed.created_seq, 0)`;
640
+ const controlState = sql`(${Dialect.jsonText(sql, sql`runs.state_json`, "$.runId")} IS NOT NULL
641
+ OR ${Dialect.jsonText(sql, sql`runs.state_json`, "$.flowId")} IS NOT NULL
642
+ OR ${Dialect.jsonText(sql, sql`runs.state_json`, "$.status")} IS NOT NULL)`;
643
+ const flowId = sql`CASE WHEN ${controlState} THEN ${Dialect.jsonText(sql, sql`runs.state_json`, "$.flowId")}
644
+ ELSE ${Dialect.jsonText(sql, sql`runs.state_json`, "$.flowName")} END`;
645
+ const ownStatus = sql`CASE WHEN ${controlState} THEN ${Dialect.jsonText(sql, sql`runs.state_json`, "$.status")}
646
+ ELSE CASE runs.status WHEN 'pending' THEN 'accepted' WHEN 'suspended' THEN 'parked' ELSE runs.status END END`;
647
+ const status = includeWaitRollup ? sql`CASE
648
+ WHEN runs.status NOT IN ('completed', 'failed', 'cancelled')
649
+ AND runs.run_id IN (SELECT ancestorId FROM human_wait_ancestry)
650
+ THEN 'waiting-approval' ELSE ${ownStatus} END` : ownStatus;
651
+ const storedParent = sql`CASE WHEN ${controlState} THEN ${Dialect.jsonText(sql, sql`runs.state_json`, "$.parentRunId")} END`;
652
+ const storedLineage = sql`CASE WHEN ${controlState} THEN ${Dialect.jsonText(sql, sql`runs.state_json`, "$.lineageId")} END`;
653
+ const parent = includeSpawn ? sql`COALESCE(runs.parent_run_id,
654
+ (SELECT parent_id FROM flows_run_parents WHERE child_id = runs.run_id ORDER BY seq LIMIT 1),
655
+ ${storedParent})` : sql`COALESCE(runs.parent_run_id, ${storedParent})`;
656
+ const lineage = sql`COALESCE(runs.lineage_id, ${storedLineage})`;
657
+ const after = request.cursor;
658
+ const conditions = [sql`1 = 1`];
659
+ if (filters?.flowId !== void 0) conditions.push(sql`${flowId} = ${filters.flowId}`);
660
+ if (filters?.status !== void 0) conditions.push(sql`${status} = ${filters.status}`);
661
+ if (filters?.terminal !== void 0) {
662
+ const isTerminal = sql`${status} IN ('completed', 'failed', 'cancelled')`;
663
+ conditions.push(filters.terminal ? isTerminal : sql`NOT (${isTerminal})`);
664
+ }
665
+ if (filters?.parentRunId !== void 0) conditions.push(sql`${parent} = ${filters.parentRunId}`);
666
+ if (filters?.lineageId !== void 0) conditions.push(sql`${lineage} = ${filters.lineageId}`);
667
+ if (filters?.since !== void 0) conditions.push(sql`runs.created_at_ms >= ${filters.since}`);
668
+ if (filters?.until !== void 0) conditions.push(sql`runs.created_at_ms < ${filters.until}`);
669
+ if (filters?.launchedBy !== void 0) {
670
+ conditions.push(sql`indexed.principal_id = ${filters.launchedBy.id}`);
671
+ if (filters.launchedBy.kind !== void 0) {
672
+ conditions.push(sql`indexed.principal_kind = ${filters.launchedBy.kind}`);
673
+ }
674
+ }
675
+ if (filters?.runIds !== void 0) {
676
+ const ids = JSON.stringify([...new Set(filters.runIds)]);
677
+ conditions.push(
678
+ sql.onDialectOrElse({
679
+ /* v8 ignore next -- PostgreSQL adapter; covered by //packages/smithers/control:postgresInventory */
680
+ pg: () => sql`runs.run_id IN (SELECT jsonb_array_elements_text(${ids}::jsonb))`,
681
+ orElse: () => sql`runs.run_id IN (SELECT value FROM json_each(${ids}))`
682
+ })
683
+ );
684
+ }
685
+ if (after !== void 0) {
686
+ conditions.push(
687
+ request.order === "newest" ? sql`(runs.created_at_ms, ${source}, ${sequence}, runs.run_id) <
688
+ (${after.createdAt}, ${after.source}, ${after.sequence}, ${after.runId})` : request.order === "oldest" ? sql`(runs.created_at_ms, ${source}, ${sequence}, runs.run_id) >
689
+ (${after.createdAt}, ${after.source}, ${after.sequence}, ${after.runId})` : sql`(${source}, ${sequence}, runs.created_at_ms, runs.run_id) >
690
+ (${after.source}, ${after.sequence}, ${after.createdAt}, ${after.runId})`
691
+ );
692
+ }
693
+ const waitAncestry = includeWaitRollup ? sql`WITH RECURSIVE human_wait_ancestry(ancestorId, depth) AS (
694
+ SELECT run_id, 0 FROM flows_runs
695
+ WHERE waiting_reason = ${ControlExecutor.humanWaitReason}
696
+ AND status NOT IN ('completed', 'failed', 'cancelled')
697
+ UNION
698
+ SELECT parent.run_id, human_wait_ancestry.depth + 1
699
+ FROM flows_runs step JOIN human_wait_ancestry ON step.run_id = human_wait_ancestry.ancestorId
700
+ JOIN flows_runs parent ON parent.run_id IN (
701
+ SELECT parent_id FROM flows_run_parents WHERE child_id = step.run_id
702
+ UNION ALL
703
+ SELECT step.parent_run_id WHERE step.parent_run_id IS NOT NULL
704
+ )
705
+ WHERE human_wait_ancestry.depth < ${maxWaitTreeDepth}
706
+ AND (parent.run_id = step.parent_run_id
707
+ OR COALESCE(${Dialect.jsonText(sql, sql`step.state_json`, "$.onParentExit")}, 'cancel') <> 'detach')
708
+ )` : sql.literal("");
709
+ return sql`
710
+ ${waitAncestry}
711
+ SELECT runs.run_id AS "runId", ${source} AS source, ${sequence} AS sequence,
712
+ runs.created_at_ms AS "createdAt"
713
+ FROM flows_runs AS runs LEFT JOIN control_runs AS indexed ON indexed.run_id = runs.run_id
714
+ WHERE ${sql.and(conditions)}
715
+ ORDER BY ${request.order === "newest" ? sql`runs.created_at_ms DESC, ${source} DESC, ${sequence} DESC, runs.run_id DESC` : request.order === "oldest" ? sql`runs.created_at_ms, ${source}, ${sequence}, runs.run_id` : sql`${source}, ${sequence}, runs.created_at_ms, runs.run_id`}
716
+ LIMIT ${request.limit + 1}
717
+ `.pipe(
718
+ probe,
719
+ import_effect.Effect.catchIf(
720
+ (error) => includeSpawn && filters?.parentRunId !== void 0 && (0, import_sqlSchemaErrors.missingTable)("flows_run_parents")(error),
721
+ () => runPageKeys(request, false, includeWaitRollup)
722
+ ),
723
+ // Without the engine's wait-tree schema, retain the ordinary page.
724
+ import_effect.Effect.catchIf(
725
+ (error) => includeWaitRollup && missingWaitTreeSchema(error),
726
+ () => runPageKeys(request, includeSpawn, false)
727
+ ),
728
+ import_effect.Effect.mapError(persistence("query runs"))
729
+ );
730
+ };
731
+ const pageByRowId = (request, table, column, operation) => import_effect.Effect.gen(function* () {
732
+ yield* (0, import_ControlRuntime.idPageLimit)(request.limit);
733
+ const through = request.through ?? Number(
734
+ (yield* sql`
735
+ SELECT MAX(rowid) AS newest FROM ${sql.literal(table)}
736
+ `.pipe(query(operation)))[0]?.newest ?? 0
737
+ );
738
+ const rows = yield* sql`
739
+ SELECT ${sql.literal(column)} AS id, rowid AS position FROM ${sql.literal(table)}
740
+ WHERE rowid > ${request.after ?? 0} AND rowid <= ${through}
741
+ ORDER BY rowid LIMIT ${request.limit + 1}
742
+ `.pipe(query(operation));
743
+ const selected = rows.slice(0, request.limit);
744
+ return rows.length > request.limit ? { ids: selected.map((row) => row.id), next: Number(selected.at(-1).position), through } : { ids: selected.map((row) => row.id), through };
745
+ });
746
+ const pagePlanIds = (request) => pageByRowId(request, "control_plans", "plan_id", "page plans");
747
+ const queryPlans = (request) => import_effect.Effect.gen(function* () {
748
+ yield* (0, import_ControlRuntime.idPageLimit)(request.limit);
749
+ const plans = [];
750
+ let after = request.after ?? 0;
751
+ while (true) {
752
+ const conditions = [
753
+ sql`rowid > ${after}`,
754
+ ...request.decision === void 0 ? [] : [sql`decision = ${request.decision}`]
755
+ ];
756
+ const rows = yield* sql`
757
+ SELECT plan_id AS "planId", card_json AS "cardJson",
758
+ decoded_input_json AS "decodedInputJson", decision, rowid AS position
759
+ FROM control_plans WHERE ${sql.and(conditions)}
760
+ ORDER BY rowid LIMIT ${planScanBatch}
761
+ `.pipe(query("page plans"));
762
+ for (const [index, row] of rows.entries()) {
763
+ after = Number(row.position);
764
+ const plan = yield* storedPlan(row);
765
+ if (request.flowId !== void 0 && plan.card.flowId !== request.flowId) continue;
766
+ plans.push(plan);
767
+ if (plans.length === request.limit) {
768
+ return index < rows.length - 1 || rows.length === planScanBatch ? { plans, next: after } : { plans };
769
+ }
770
+ }
771
+ if (rows.length < planScanBatch) return { plans };
772
+ }
773
+ });
774
+ const launcherOf = (runId) => import_effect.Effect.map(
775
+ sql`
776
+ SELECT principal_id AS id, principal_kind AS kind FROM control_runs WHERE run_id = ${runId}
777
+ `.pipe(query("read a run launcher")),
778
+ (rows) => {
779
+ const row = rows[0];
780
+ return row === void 0 || row.id === null || row.kind === null ? void 0 : { id: row.id, kind: row.kind };
781
+ }
782
+ );
783
+ const messages = (runId, kind, schema) => sql`
784
+ SELECT payload_json AS "payloadJson" FROM control_run_messages
785
+ WHERE run_id = ${runId} AND kind = ${kind} ORDER BY seq
786
+ `.pipe(
787
+ query("read run messages"),
788
+ import_effect.Effect.flatMap(
789
+ (rows) => import_effect.Effect.forEach(
790
+ rows,
791
+ (row) => decodeStoredJson(`control_run_messages.payload_json as ${kind}`, schema, row.payloadJson)
792
+ )
793
+ )
794
+ );
795
+ const appendMessage = (runId, kind, payload) => import_effect.Effect.gen(function* () {
796
+ yield* requireRow(runId);
797
+ yield* sql`
798
+ INSERT INTO control_run_messages (run_id, kind, payload_json)
799
+ VALUES (${runId}, ${kind}, ${JSON.stringify(payload)})
800
+ `.pipe(import_effect.Effect.mapError(persistence("append a run message")));
801
+ });
802
+ const service = (0, import_ControlRuntime.make)({
803
+ authorizeApproval,
804
+ plan: import_effect.Effect.fn("SqlControlRuntime.plan")(function* (input) {
805
+ const flow = (yield* readFlows).get(input.flowId);
806
+ if (flow === void 0) {
807
+ return yield* new import_ControlError.FlowNotFound({ flowId: input.flowId });
808
+ }
809
+ const requestFingerprint = yield* import_effect.Effect.try({
810
+ try: () => (0, import_planning.planFingerprint)(input),
811
+ catch: (cause) => new import_ControlError.InvalidInput({ issue: (0, import_issues.canonicalIssue)(cause) })
812
+ });
813
+ if (input.idempotencyKey !== void 0) {
814
+ const prior = yield* sql`
815
+ SELECT fingerprint, plan_id AS "planId" FROM control_plan_keys
816
+ WHERE idempotency_key = ${input.idempotencyKey}
817
+ `.pipe(query("read a plan key"));
818
+ const found = prior[0];
819
+ if (found !== void 0) {
820
+ if (found.fingerprint !== requestFingerprint) {
821
+ return yield* new import_ControlError.InvalidInput({
822
+ issue: `idempotency key ${input.idempotencyKey} was used for another plan`
823
+ });
824
+ }
825
+ const stored = yield* readPlan(found.planId);
826
+ if (import_effect.Option.isSome(stored)) {
827
+ const decoded2 = yield* storedPlan(stored.value);
828
+ return { card: decoded2.card, created: false };
829
+ }
830
+ }
831
+ }
832
+ const decoded = yield* flow.decode?.(input.input) ?? import_effect.Effect.try({
833
+ try: () => {
834
+ (0, import_planning.canonical)(input.input);
835
+ return input.input;
836
+ },
837
+ catch: (cause) => new import_ControlError.InvalidInput({ issue: (0, import_issues.canonicalIssue)(cause) })
838
+ });
839
+ const planId = `plan-${yield* nextSequence("plan")}`;
840
+ const handoff = flow.plan === void 0 ? void 0 : yield* flow.plan(decoded, planId);
841
+ const card = yield* (0, import_planning.planCard)({
842
+ planId,
843
+ flowId: input.flowId,
844
+ decodedInput: decoded,
845
+ envelope: (0, import_planning.budgeted)(flow.envelope, input.budget),
846
+ deployClass: flow.deployClass,
847
+ executionDigest: flow.executionDigest,
848
+ handoff,
849
+ idempotencyKey: input.idempotencyKey
850
+ }).pipe(import_effect.Effect.provideService(import_effect.Crypto.Crypto, crypto));
851
+ const identity = approvalIdentity(card.approval.target);
852
+ const outcome = yield* writer.write(import_effect.Effect.gen(function* () {
853
+ if (input.idempotencyKey !== void 0) {
854
+ yield* sql`
855
+ INSERT INTO control_plan_keys (idempotency_key, fingerprint, plan_id)
856
+ VALUES (${input.idempotencyKey}, ${requestFingerprint}, ${planId})
857
+ ON CONFLICT (idempotency_key) DO NOTHING
858
+ `;
859
+ const settled = yield* sql`
860
+ SELECT fingerprint, plan_id AS "planId" FROM control_plan_keys
861
+ WHERE idempotency_key = ${input.idempotencyKey}
862
+ `;
863
+ const holder = settled[0];
864
+ if (holder !== void 0 && holder.planId !== planId) {
865
+ return { _tag: "raced", holder };
866
+ }
867
+ }
868
+ const decodedJson = JSON.stringify(decoded ?? null);
869
+ yield* sql`
870
+ INSERT INTO control_plans (plan_id, card_json, decoded_input_json, decision)
871
+ VALUES (${planId}, ${JSON.stringify(card)}, ${decodedJson}, 'pending')
872
+ `;
873
+ yield* sql`
874
+ INSERT INTO control_tokens (
875
+ target_tag, run_id, target_id, token_id, target_json, resolved, decision_principal_json
876
+ )
877
+ VALUES (
878
+ ${identity.targetTag}, ${identity.runId}, ${identity.targetId}, ${planId},
879
+ ${JSON.stringify(card.approval.target)}, 0, NULL
880
+ )
881
+ `;
882
+ return { _tag: "stored" };
883
+ })).pipe(import_effect.Effect.mapError(persistence("store a plan")));
884
+ if (outcome._tag === "raced") {
885
+ if (outcome.holder.fingerprint !== requestFingerprint) {
886
+ return yield* new import_ControlError.InvalidInput({
887
+ issue: `idempotency key ${String(input.idempotencyKey)} was used for another plan`
888
+ });
889
+ }
890
+ const stored = yield* readPlan(outcome.holder.planId);
891
+ if (import_effect.Option.isNone(stored)) {
892
+ return yield* new import_ControlError.PersistenceError({
893
+ operation: "read a plan",
894
+ message: `plan key ${String(input.idempotencyKey)} names plan ${outcome.holder.planId}, which is absent`
895
+ });
896
+ }
897
+ const decodedHolder = yield* storedPlan(stored.value);
898
+ return { card: decodedHolder.card, created: false };
899
+ }
900
+ return { card, created: true };
901
+ }),
902
+ getPlan: import_effect.Effect.fn("SqlControlRuntime.getPlan")(
903
+ (planId) => import_effect.Effect.flatMap(requirePlan(planId), storedPlan)
904
+ ),
905
+ pagePlanIds,
906
+ queryPlans,
907
+ lookupApproval: import_effect.Effect.fn("SqlControlRuntime.lookupApproval")(function* (target) {
908
+ const tokenId = target._tag === "Plan" ? target.planId : target.requestId;
909
+ const identity = approvalIdentity(target);
910
+ const rows = yield* sql`
911
+ SELECT token_id AS "tokenId", target_json AS "targetJson", resolved,
912
+ decision_principal_json AS "decisionPrincipalJson", decision_json AS "decisionJson"
913
+ FROM control_tokens
914
+ WHERE target_tag = ${identity.targetTag}
915
+ AND run_id = ${identity.runId}
916
+ AND target_id = ${identity.targetId}
917
+ `.pipe(query("read an approval token"));
918
+ const row = rows[0];
919
+ if (row === void 0) {
920
+ return yield* target._tag === "Node" ? new import_ControlError.RunNotFound({ runId: target.runId }) : new import_ControlError.PlanNotFound({ planId: target.planId });
921
+ }
922
+ const stored = yield* decodeStoredJson("control_tokens.target_json", import_ControlSchema.ApprovalTarget, row.targetJson);
923
+ if (!sameApprovalIdentity(stored, target)) {
924
+ return yield* new import_ControlError.PersistenceError({
925
+ operation: "validate an approval token",
926
+ message: "The stored approval target does not match its identity"
927
+ });
928
+ }
929
+ if (stored.digest !== target.digest) {
930
+ return yield* new import_ControlError.PlanDigestMismatch({
931
+ planId: tokenId,
932
+ expected: stored.digest,
933
+ actual: target.digest
934
+ });
935
+ }
936
+ if (!(0, import_planning.sameEnvelope)(stored.envelope, target.envelope)) {
937
+ return yield* new import_ControlError.EnvelopeMismatch({
938
+ planId: tokenId,
939
+ expected: (0, import_planning.canonical)(stored.envelope),
940
+ actual: (0, import_planning.canonical)(target.envelope)
941
+ });
942
+ }
943
+ const token = yield* tokenFromRow(row, stored);
944
+ if (token._tag !== "Pending") return yield* new import_ControlError.AlreadyResolved({ requestId: tokenId });
945
+ return token;
946
+ }),
947
+ registerApproval: import_effect.Effect.fn("SqlControlRuntime.registerApproval")(function* (target) {
948
+ yield* requireRow(target.runId);
949
+ const identity = approvalIdentity(target);
950
+ yield* sql`
951
+ INSERT INTO control_tokens (
952
+ target_tag, run_id, target_id, token_id, target_json, resolved, decision_principal_json
953
+ )
954
+ VALUES (
955
+ ${identity.targetTag}, ${identity.runId}, ${identity.targetId}, ${target.requestId},
956
+ ${JSON.stringify(target)}, 0, NULL
957
+ )
958
+ ON CONFLICT (target_tag, run_id, target_id) DO NOTHING
959
+ `.pipe(import_effect.Effect.mapError(persistence("register an approval token")));
960
+ const rows = yield* sql`
961
+ SELECT token_id AS "tokenId", target_json AS "targetJson", resolved,
962
+ decision_principal_json AS "decisionPrincipalJson", decision_json AS "decisionJson"
963
+ FROM control_tokens
964
+ WHERE target_tag = ${identity.targetTag}
965
+ AND run_id = ${identity.runId}
966
+ AND target_id = ${identity.targetId}
967
+ `.pipe(query("read an approval token"));
968
+ const row = rows[0];
969
+ if (row === void 0) {
970
+ return yield* import_effect.Effect.fail(
971
+ new import_ControlError.PersistenceError({
972
+ operation: "register an approval token",
973
+ message: "A registered approval token could not be read back"
974
+ })
975
+ );
976
+ }
977
+ const stored = yield* decodeStoredJson("control_tokens.target_json", import_ControlSchema.ApprovalTarget, row.targetJson);
978
+ if (!sameApprovalIdentity(stored, target)) {
979
+ return yield* new import_ControlError.PersistenceError({
980
+ operation: "validate an approval token",
981
+ message: "The stored approval target does not match its identity"
982
+ });
983
+ }
984
+ if (stored.digest !== target.digest) {
985
+ return yield* new import_ControlError.PlanDigestMismatch({
986
+ planId: target.requestId,
987
+ expected: stored.digest,
988
+ actual: target.digest
989
+ });
990
+ }
991
+ if (!(0, import_planning.sameEnvelope)(stored.envelope, target.envelope)) {
992
+ return yield* new import_ControlError.EnvelopeMismatch({
993
+ planId: target.requestId,
994
+ expected: (0, import_planning.canonical)(stored.envelope),
995
+ actual: (0, import_planning.canonical)(target.envelope)
996
+ });
997
+ }
998
+ return yield* tokenFromRow(row, stored);
999
+ }),
1000
+ installBulkGrant: import_effect.Effect.fn("SqlControlRuntime.installBulkGrant")(function* (token, envelope, scope) {
1001
+ const timestamp = yield* now;
1002
+ const identity = approvalIdentity(token.target);
1003
+ yield* sql`
1004
+ INSERT INTO control_grants (
1005
+ target_tag, run_id, target_id, token_id, envelope_json, scope, installed_at_ms
1006
+ )
1007
+ VALUES (
1008
+ ${identity.targetTag}, ${identity.runId}, ${identity.targetId}, ${token.tokenId},
1009
+ ${JSON.stringify(envelope)}, ${scope}, ${timestamp}
1010
+ )
1011
+ ON CONFLICT (target_tag, run_id, target_id) DO NOTHING
1012
+ `.pipe(import_effect.Effect.mapError(persistence("install a grant")));
1013
+ }),
1014
+ resolveApproval: import_effect.Effect.fn("SqlControlRuntime.resolveApproval")(function* (token, decision, principal, scope = "once") {
1015
+ const requested = yield* import_effect.Effect.try({
1016
+ try: () => structuredClone({ target: token.target, principal, tokenId: token.tokenId }),
1017
+ catch: () => new import_ControlError.PersistenceError({
1018
+ operation: "record an approval decision",
1019
+ message: "The approval request cannot be captured"
1020
+ })
1021
+ });
1022
+ const identity = approvalIdentity(requested.target);
1023
+ const requestedPrincipal = requested.principal;
1024
+ const tokenId = requested.tokenId;
1025
+ const decidedAt = yield* now;
1026
+ const answer = yield* decodeStoredValue(
1027
+ "approval decision",
1028
+ import_ControlRuntime.ApprovalDecision,
1029
+ decision === "approved" ? { _tag: "Approved", decisionPrincipal: requestedPrincipal, decidedAt, scope } : { _tag: "Denied", decisionPrincipal: requestedPrincipal, decidedAt }
1030
+ );
1031
+ const resolved = yield* writer.write(import_effect.Effect.gen(function* () {
1032
+ yield* authorizeApproval({ principal: requestedPrincipal, target: requested.target, decision, scope });
1033
+ const rows = yield* sql`
1034
+ UPDATE control_tokens
1035
+ SET resolved = 1, decision_principal_json = ${JSON.stringify(requestedPrincipal)},
1036
+ decision_json = ${JSON.stringify(answer)}
1037
+ WHERE target_tag = ${identity.targetTag}
1038
+ AND run_id = ${identity.runId}
1039
+ AND target_id = ${identity.targetId}
1040
+ AND resolved = 0
1041
+ RETURNING token_id AS "tokenId"
1042
+ `;
1043
+ if (rows.length === 0) return false;
1044
+ if (identity.targetTag === "Plan") {
1045
+ yield* sql`UPDATE control_plans SET decision = ${decision} WHERE plan_id = ${identity.targetId}`;
1046
+ }
1047
+ return true;
1048
+ })).pipe(import_effect.Effect.mapError(
1049
+ (error) => error instanceof import_ControlError.Unauthorized || error instanceof import_ControlError.PersistenceError ? error : persistence("resolve an approval")(error)
1050
+ ));
1051
+ if (!resolved) return yield* new import_ControlError.AlreadyResolved({ requestId: tokenId });
1052
+ }),
1053
+ launch: import_effect.Effect.fn("SqlControlRuntime.launch")(function* (planId, requestedDigest, envelope, principal, reservedRunId) {
1054
+ const row = yield* requirePlan(planId);
1055
+ const plan = yield* storedPlan(row);
1056
+ if (plan.card.digest !== requestedDigest) {
1057
+ return yield* new import_ControlError.PlanDigestMismatch({
1058
+ planId,
1059
+ expected: plan.card.digest,
1060
+ actual: requestedDigest
1061
+ });
1062
+ }
1063
+ if (!(0, import_planning.sameEnvelope)(plan.card.envelope, envelope)) {
1064
+ return yield* new import_ControlError.EnvelopeMismatch({
1065
+ planId,
1066
+ expected: (0, import_planning.canonical)(plan.card.envelope),
1067
+ actual: (0, import_planning.canonical)(envelope)
1068
+ });
1069
+ }
1070
+ if (plan.decision === "pending") {
1071
+ const parked = {
1072
+ _tag: "Parked",
1073
+ receipt: {
1074
+ _tag: "Parked",
1075
+ receiptId: `launch:${planId}`,
1076
+ planId,
1077
+ status: "waiting-approval"
1078
+ }
1079
+ };
1080
+ return parked;
1081
+ }
1082
+ if (plan.decision !== "approved") return yield* new import_ControlError.PlanDenied({ planId });
1083
+ const sequence = yield* nextSequence("run");
1084
+ const runId = reservedRunId ?? `run-${sequence}`;
1085
+ const timestamp = yield* now;
1086
+ const claimant = { ...owner, nonce: randomId() };
1087
+ const summary = {
1088
+ runId,
1089
+ flowId: plan.card.flowId,
1090
+ status: "accepted",
1091
+ planId,
1092
+ planDigest: plan.card.digest,
1093
+ ...plan.card.executionDigest === void 0 ? {} : { executionDigest: plan.card.executionDigest },
1094
+ ...options.engineVersion === void 0 ? {} : { engineVersion: options.engineVersion },
1095
+ ownerId: JSON.stringify(claimant),
1096
+ ...plan.card.envelope.budget.deadline === void 0 ? {} : { deadlineAt: timestamp + plan.card.envelope.budget.deadline },
1097
+ createdAt: timestamp,
1098
+ updatedAt: timestamp
1099
+ };
1100
+ yield* runStore.create(runId, JSON.stringify(summary)).pipe(
1101
+ import_effect.Effect.mapError(persistence("create a run"))
1102
+ );
1103
+ yield* sql`INSERT INTO control_runs (run_id, created_seq, principal_id, principal_kind)
1104
+ VALUES (${runId}, ${sequence}, ${principal?.id ?? null}, ${principal?.kind ?? null})`.pipe(
1105
+ import_effect.Effect.mapError(persistence("index a run"))
1106
+ );
1107
+ const outcome = yield* runStore.claimAndOwn(
1108
+ runId,
1109
+ { status: "pending", owner: null, heartbeatAtMs: null },
1110
+ claimant,
1111
+ timestamp
1112
+ ).pipe(import_effect.Effect.mapError(persistence("claim a new run")));
1113
+ if (outcome._tag !== "Activated") return yield* new import_ControlError.ClaimLost({ runId });
1114
+ const started = {
1115
+ _tag: "Started",
1116
+ receipt: (0, import_planning.accepted)(`launch:${planId}:${runId}`, runId),
1117
+ run: withLauncher(summary, principal)
1118
+ };
1119
+ return started;
1120
+ }),
1121
+ getRun: import_effect.Effect.fn("SqlControlRuntime.getRun")(
1122
+ (runId) => import_effect.Effect.gen(function* () {
1123
+ const row = yield* requireRow(runId);
1124
+ return yield* summaryFrom(row, yield* ancestryIndex(yield* ancestorChain(runId)));
1125
+ })
1126
+ ),
1127
+ /**
1128
+ * Every durable run in `flows_runs` insertion order: `control_runs`
1129
+ * indexes only the runs this plane launched, while a child, a fork, and a
1130
+ * later trampoline round are created by the engine straight into
1131
+ * `flows_runs`. One indexed seek per page, with no row decoded, so a
1132
+ * caller that walks every run holds one page of ids at a time.
1133
+ */
1134
+ pageRunIds: (request) => pageByRowId(request, "flows_runs", "run_id", "page runs"),
1135
+ queryRuns: import_effect.Effect.fn("SqlControlRuntime.queryRuns")(function* (request) {
1136
+ if (!Number.isSafeInteger(request.limit) || request.limit < 1 || request.limit > 500) {
1137
+ return yield* new import_ControlError.InvalidInput({ issue: "limit: must be an integer between 1 and 500" });
1138
+ }
1139
+ const keys = yield* runPageKeys(request);
1140
+ const selected = keys.slice(0, request.limit);
1141
+ if (selected.length === 0) return { items: [] };
1142
+ const chains = yield* import_effect.Effect.forEach(selected, (key) => ancestorChain(key.runId));
1143
+ const ancestry = yield* ancestryIndex([...new Set(chains.flat())]);
1144
+ const summaries = yield* import_effect.Effect.forEach(selected, (key) => requireRow(key.runId).pipe(
1145
+ import_effect.Effect.flatMap((row) => import_effect.Effect.map(summaryFrom(row, ancestry), import_effect.Option.some)),
1146
+ import_effect.Effect.catchTag("/control/RunNotFound", () => import_effect.Effect.succeed(import_effect.Option.none()))
1147
+ ));
1148
+ return {
1149
+ items: summaries.filter(import_effect.Option.isSome).map((summary) => summary.value),
1150
+ ...keys.length > request.limit ? { nextCursor: selected[selected.length - 1] } : {}
1151
+ };
1152
+ }),
1153
+ listFlows: import_effect.Effect.fn("SqlControlRuntime.listFlows")(
1154
+ () => import_effect.Effect.map(readFlows, (flows) => Array.from(flows.values(), (flow) => ({
1155
+ flowId: flow.flowId,
1156
+ description: flow.description
1157
+ })))
1158
+ )(),
1159
+ deliverSignal: import_effect.Effect.fn("SqlControlRuntime.deliverSignal")(
1160
+ (runId, signal) => (
1161
+ // Durable delivery, and deliberately no resumption: a signal records a
1162
+ // fact, it does not decide who runs next.
1163
+ appendMessage(runId, "signal", signal)
1164
+ )
1165
+ ),
1166
+ admitSignal: import_effect.Effect.fn("SqlControlRuntime.admitSignal")(function* (commandId, runId, signal, principal) {
1167
+ yield* requireRow(runId);
1168
+ const payloadJson = JSON.stringify(signal);
1169
+ const principalJson = principal === void 0 ? null : JSON.stringify(principal);
1170
+ yield* writer.write(sql`INSERT INTO control_signal_commands (command_id, run_id, payload_json, principal_json)
1171
+ VALUES (${commandId}, ${runId}, ${payloadJson}, ${principalJson}) ON CONFLICT(command_id) DO NOTHING`).pipe(
1172
+ import_effect.Effect.mapError(persistence("admit signal command"))
1173
+ );
1174
+ }),
1175
+ signalCommand: import_effect.Effect.fn("SqlControlRuntime.signalCommand")(function* (commandId) {
1176
+ const rows = yield* sql`
1177
+ SELECT command_id AS "commandId", run_id AS "runId", payload_json AS "payloadJson", wait_token AS token, state,
1178
+ principal_json AS "principalJson"
1179
+ FROM control_signal_commands WHERE command_id = ${commandId}`.pipe(query("read signal command"));
1180
+ const row = rows[0];
1181
+ if (row === void 0) return void 0;
1182
+ return {
1183
+ commandId: row.commandId,
1184
+ runId: row.runId,
1185
+ token: row.token,
1186
+ state: row.state,
1187
+ signal: yield* decodeStoredJson("control_signal_commands.payload_json", import_ControlSchema.SignalPayload, row.payloadJson),
1188
+ ...yield* signalPrincipal(row.principalJson)
1189
+ };
1190
+ }),
1191
+ pendingSignals: import_effect.Effect.gen(function* () {
1192
+ const read = (after) => sql`
1193
+ SELECT seq, command_id AS "commandId", run_id AS "runId", payload_json AS "payloadJson", wait_token AS token, state,
1194
+ principal_json AS "principalJson"
1195
+ FROM control_signal_commands WHERE state = 'pending' AND seq > ${after} ORDER BY seq LIMIT 100`.pipe(
1196
+ query("read pending signals")
1197
+ );
1198
+ let rows = yield* read(pendingSignalCursor);
1199
+ if (rows.length === 0 && pendingSignalCursor !== 0) rows = yield* read(0);
1200
+ pendingSignalCursor = rows.at(-1)?.seq ?? 0;
1201
+ const decoded = yield* import_effect.Effect.forEach(
1202
+ rows,
1203
+ (row) => import_effect.Effect.all([
1204
+ decodeStoredJson("control_signal_commands.payload_json", import_ControlSchema.SignalPayload, row.payloadJson),
1205
+ signalPrincipal(row.principalJson)
1206
+ ]).pipe(
1207
+ import_effect.Effect.map(
1208
+ ([signal, principal]) => import_effect.Option.some({
1209
+ commandId: row.commandId,
1210
+ runId: row.runId,
1211
+ token: row.token,
1212
+ state: row.state,
1213
+ signal,
1214
+ ...principal
1215
+ })
1216
+ ),
1217
+ import_effect.Effect.catch(
1218
+ (error) => writer.write(
1219
+ sql`UPDATE control_signal_commands SET state = 'rejected' WHERE command_id = ${row.commandId} AND state = 'pending'`
1220
+ ).pipe(
1221
+ import_effect.Effect.mapError(persistence("quarantine malformed signal")),
1222
+ import_effect.Effect.andThen(
1223
+ import_effect.Effect.logWarning("Malformed admitted signal rejected", { commandId: row.commandId, error })
1224
+ ),
1225
+ import_effect.Effect.as(import_effect.Option.none())
1226
+ )
1227
+ )
1228
+ )
1229
+ );
1230
+ return decoded.filter(import_effect.Option.isSome).map((item) => item.value);
1231
+ }),
1232
+ bindSignal: import_effect.Effect.fn("SqlControlRuntime.bindSignal")(
1233
+ (commandId, token) => writer.write(import_effect.Effect.gen(function* () {
1234
+ 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})`;
1235
+ const rows = yield* sql`SELECT wait_token AS token FROM control_signal_commands WHERE command_id = ${commandId}`;
1236
+ if (rows[0] === void 0) {
1237
+ return yield* new import_ControlError.PersistenceError({
1238
+ operation: "bind signal",
1239
+ message: `No pending signal command ${commandId}`
1240
+ });
1241
+ }
1242
+ return rows[0].token;
1243
+ })).pipe(import_effect.Effect.mapError(persistence("bind signal")))
1244
+ ),
1245
+ settleSignal: import_effect.Effect.fn("SqlControlRuntime.settleSignal")(
1246
+ (commandId, state) => writer.write(
1247
+ sql`UPDATE control_signal_commands SET state = ${state} WHERE command_id = ${commandId} AND state = 'pending'`
1248
+ ).pipe(import_effect.Effect.asVoid, import_effect.Effect.mapError(persistence("settle signal")))
1249
+ ),
1250
+ deliveredSignals: import_effect.Effect.fn("SqlControlRuntime.deliveredSignals")(function* (runId) {
1251
+ yield* requireRow(runId);
1252
+ const legacy = yield* messages(runId, "signal", import_ControlSchema.SignalPayload);
1253
+ const rows = yield* sql`SELECT payload_json AS "payloadJson" FROM control_signal_commands WHERE run_id = ${runId} AND state != 'rejected' ORDER BY seq`.pipe(query("read admitted signals"));
1254
+ return [
1255
+ ...legacy,
1256
+ ...yield* import_effect.Effect.forEach(
1257
+ rows,
1258
+ (row) => decodeStoredJson("control_signal_commands.payload_json", import_ControlSchema.SignalPayload, row.payloadJson)
1259
+ )
1260
+ ];
1261
+ }),
1262
+ requestResume: import_effect.Effect.fn("SqlControlRuntime.requestResume")(function* (runId, options2) {
1263
+ const summary = yield* summaryOf(yield* requireRow(runId));
1264
+ if (terminal(summary.status)) {
1265
+ return yield* new import_ControlError.InvalidInput({
1266
+ issue: `run ${runId} is ${summary.status} and cannot take a resume`
1267
+ });
1268
+ }
1269
+ const sequence = yield* nextSequence("resume");
1270
+ const timestamp = yield* now;
1271
+ const consent = options2?.consent ?? null;
1272
+ yield* writer.write(sql`
1273
+ INSERT INTO control_run_resumes (run_id, requested_seq, requested_at_ms, consent_seq)
1274
+ VALUES (${runId}, ${sequence}, ${timestamp}, ${consent})
1275
+ ON CONFLICT (run_id) DO UPDATE SET
1276
+ requested_seq = excluded.requested_seq,
1277
+ requested_at_ms = excluded.requested_at_ms,
1278
+ consent_seq = COALESCE(excluded.consent_seq, control_run_resumes.consent_seq)
1279
+ `).pipe(import_effect.Effect.mapError(persistence("record a resume delegation")));
1280
+ return sequence;
1281
+ }),
1282
+ // Terminal runs are filtered in SQL: a delegation nobody will ever take
1283
+ // up must not keep appearing in every host's poll.
1284
+ pendingResumes: sql`
1285
+ SELECT resumes.run_id AS "runId",
1286
+ resumes.requested_seq AS "requestedSeq",
1287
+ resumes.requested_at_ms AS "requestedAtMs",
1288
+ resumes.consent_seq AS "consentSeq"
1289
+ FROM control_run_resumes AS resumes
1290
+ JOIN flows_runs AS runs ON runs.run_id = resumes.run_id
1291
+ WHERE runs.status NOT IN ('completed', 'failed', 'cancelled')
1292
+ ORDER BY resumes.requested_seq
1293
+ `.pipe(
1294
+ query("read pending resumes"),
1295
+ import_effect.Effect.map(
1296
+ (rows) => rows.map((row) => ({
1297
+ runId: row.runId,
1298
+ sequence: Number(row.requestedSeq),
1299
+ requestedAtMs: Number(row.requestedAtMs),
1300
+ ...row.consentSeq === null ? {} : { consent: Number(row.consentSeq) }
1301
+ }))
1302
+ )
1303
+ ),
1304
+ clearResume: import_effect.Effect.fn("SqlControlRuntime.clearResume")(
1305
+ (runId, sequence) => writer.write(sql`
1306
+ DELETE FROM control_run_resumes WHERE run_id = ${runId} AND requested_seq = ${sequence}
1307
+ `).pipe(import_effect.Effect.mapError(persistence("clear a resume delegation")), import_effect.Effect.asVoid)
1308
+ ),
1309
+ registerFiber: import_effect.Effect.fn("SqlControlRuntime.registerFiber")(function* (runId, fiber) {
1310
+ yield* requireRow(runId);
1311
+ ActiveFibers.register(fibers, runId, fiber);
1312
+ }),
1313
+ interrupt: import_effect.Effect.fn("SqlControlRuntime.interrupt")(function* (runId, settle = (effect) => effect) {
1314
+ const row = yield* requireRow(runId);
1315
+ const summary = yield* summaryOf(row);
1316
+ if (terminal(summary.status)) return summary;
1317
+ if (!ownedByUs(row)) return yield* new import_ControlError.ClaimLost({ runId });
1318
+ const fiber = fibers.get(runId);
1319
+ if (fiber !== void 0) yield* import_effect.Fiber.interrupt(fiber);
1320
+ if (fibers.get(runId) === fiber) fibers.delete(runId);
1321
+ return yield* settle(
1322
+ writer.write(import_effect.Effect.gen(function* () {
1323
+ const current = yield* summaryOf(yield* requireRow(runId));
1324
+ if (terminal(current.status)) return current;
1325
+ return yield* transition(runId, row.owner, current, "cancelled");
1326
+ })).pipe(
1327
+ import_effect.Effect.catchTag(
1328
+ "@smthrs/database/DatabaseError",
1329
+ (error) => import_effect.Effect.fail(persistence("settle an interrupted run")(error))
1330
+ )
1331
+ )
1332
+ );
1333
+ }),
1334
+ codeDrift: import_effect.Effect.fn("SqlControlRuntime.codeDrift")(function* (runId) {
1335
+ const summary = yield* recordedCode(yield* requireRow(runId));
1336
+ const current = (yield* readCurrentFlows).get(summary.flowId);
1337
+ const pinned = summary.executionDigest === void 0 || options.pinnedFlow === void 0 ? void 0 : yield* options.pinnedFlow(summary.flowId, summary.executionDigest);
1338
+ return (0, import_planning.codeDriftOf)(
1339
+ summary,
1340
+ pinned === true ? { executionDigest: summary.executionDigest } : pinned === false ? void 0 : current,
1341
+ options.engineVersion
1342
+ );
1343
+ }),
1344
+ recordedCode: import_effect.Effect.fn("SqlControlRuntime.recordedCode")(function* (runId) {
1345
+ const { executionDigest, engineVersion } = yield* recordedCode(yield* requireRow(runId));
1346
+ return { executionDigest, engineVersion };
1347
+ }),
1348
+ resume: import_effect.Effect.fn("SqlControlRuntime.resume")((runId, resumeOptions) => resumeRun(runId, resumeOptions?.scope)),
1349
+ resumeAdopting: import_effect.Effect.fn("SqlControlRuntime.resumeAdopting")((runId, resumeOptions) => resumeRun(runId, resumeOptions?.scope, adoptable)),
1350
+ claimFence: import_effect.Effect.fn("SqlControlRuntime.claimFence")(function* (runId) {
1351
+ const row = yield* requireRow(runId);
1352
+ if (!ownedByUs(row)) return yield* new import_ControlError.ClaimLost({ runId });
1353
+ return JSON.stringify(row.owner);
1354
+ }),
1355
+ releasePending: import_effect.Effect.fn("SqlControlRuntime.releasePending")(function* (runId, fence) {
1356
+ const row = yield* requireRow(runId);
1357
+ const presented = yield* decodeStoredJson("control fence", import_run_store.Ownership.OwnerId, fence).pipe(
1358
+ import_effect.Effect.mapError(() => new import_ControlError.ClaimLost({ runId }))
1359
+ );
1360
+ const timestamp = yield* now;
1361
+ const next = {
1362
+ ...yield* summaryOf(row),
1363
+ status: "accepted",
1364
+ ownerId: void 0,
1365
+ parkedBy: void 0,
1366
+ updatedAt: timestamp
1367
+ };
1368
+ const outcome = yield* runStore.transitionOwned(
1369
+ runId,
1370
+ presented,
1371
+ "suspended",
1372
+ JSON.stringify(next)
1373
+ ).pipe(import_effect.Effect.mapError(persistence("release a pending run")));
1374
+ if (outcome._tag === "NotFound") return yield* new import_ControlError.RunNotFound({ runId });
1375
+ if (outcome._tag !== "Transitioned") return yield* new import_ControlError.ClaimLost({ runId });
1376
+ return next;
1377
+ }),
1378
+ writeStatus: import_effect.Effect.fn("SqlControlRuntime.writeStatus")(function* (runId, fence, status) {
1379
+ const row = yield* requireRow(runId);
1380
+ const presented = yield* decodeStoredJson("control fence", import_run_store.Ownership.OwnerId, fence).pipe(
1381
+ import_effect.Effect.mapError(() => new import_ControlError.ClaimLost({ runId }))
1382
+ );
1383
+ return yield* transition(runId, presented, yield* summaryOf(row), status);
1384
+ }),
1385
+ /**
1386
+ * The submitted identity wins, and only the clock is the runtime's.
1387
+ *
1388
+ * `Control.RunMutationInput` states the order: the runtime "supplies its
1389
+ * own principal when the caller names none". The submitted one is the
1390
+ * identity the server authenticated at its boundary, so a composition
1391
+ * default that overrode it would rename every remote operator to
1392
+ * whatever this process was built with.
1393
+ */
1394
+ stampPrincipal: import_effect.Effect.fn("SqlControlRuntime.stampPrincipal")(function* (submitted) {
1395
+ const timestamp = yield* now;
1396
+ return {
1397
+ id: submitted?.id ?? options.principal?.id ?? "local",
1398
+ kind: submitted?.kind ?? options.principal?.kind ?? "operator",
1399
+ stampedAt: timestamp
1400
+ };
1401
+ }),
1402
+ lookupMutation: import_effect.Effect.fn("SqlControlRuntime.lookupMutation")(function* (key, fingerprint) {
1403
+ const rows = yield* sql`
1404
+ SELECT fingerprint, receipt_json AS "receiptJson" FROM control_mutations WHERE mutation_key = ${key}
1405
+ `.pipe(query("read a mutation"));
1406
+ const row = rows[0];
1407
+ if (row === void 0) return void 0;
1408
+ if (row.fingerprint !== fingerprint) {
1409
+ return { _tag: "Conflict", message: `idempotency key ${key} was used for another mutation` };
1410
+ }
1411
+ const receipt = yield* decodeStoredJson("control_mutations.receipt_json", import_ControlSchema.Receipt, row.receiptJson);
1412
+ return (0, import_planning.alreadyApplied)(key, receipt);
1413
+ }),
1414
+ recordMutation: import_effect.Effect.fn("SqlControlRuntime.recordMutation")(
1415
+ (key, fingerprint, receipt) => writer.write(import_effect.Effect.gen(function* () {
1416
+ yield* sql`
1417
+ INSERT INTO control_mutations (mutation_key, fingerprint, receipt_json)
1418
+ VALUES (${key}, ${fingerprint}, ${JSON.stringify(receipt)})
1419
+ ON CONFLICT (mutation_key) DO NOTHING
1420
+ `;
1421
+ const rows = yield* sql`
1422
+ SELECT fingerprint, receipt_json AS "receiptJson"
1423
+ FROM control_mutations WHERE mutation_key = ${key}
1424
+ `;
1425
+ const stored = rows[0];
1426
+ if (stored === void 0 || stored.fingerprint !== fingerprint || stored.receiptJson !== JSON.stringify(receipt)) {
1427
+ return yield* import_effect.Effect.fail(
1428
+ new import_ControlError.PersistenceError({
1429
+ operation: "record a mutation",
1430
+ message: `Idempotency key ${key} was already settled by another mutation`
1431
+ })
1432
+ );
1433
+ }
1434
+ })).pipe(
1435
+ import_effect.Effect.asVoid,
1436
+ import_effect.Effect.mapError(
1437
+ (cause) => cause instanceof import_ControlError.PersistenceError ? cause : persistence("record a mutation")(cause)
1438
+ )
1439
+ )
1440
+ ),
1441
+ claimRunKey: import_effect.Effect.fn("SqlControlRuntime.claimRunKey")((key, fingerprint) => {
1442
+ const claimant = randomId();
1443
+ return writer.write(import_effect.Effect.gen(function* () {
1444
+ yield* sql`
1445
+ INSERT INTO control_run_keys (idempotency_key, fingerprint, claimant)
1446
+ VALUES (${key}, ${fingerprint}, ${claimant})
1447
+ ON CONFLICT (idempotency_key) DO NOTHING
1448
+ `;
1449
+ const holders = yield* sql`
1450
+ SELECT fingerprint, claimant FROM control_run_keys
1451
+ WHERE idempotency_key = ${key}
1452
+ `;
1453
+ const holder = holders[0];
1454
+ if (holder === void 0) {
1455
+ return yield* new import_ControlError.PersistenceError({
1456
+ operation: "claim a run key",
1457
+ message: `Run key ${key} disappeared while it was being claimed`
1458
+ });
1459
+ }
1460
+ if (holder.fingerprint !== fingerprint) {
1461
+ return yield* new import_ControlError.InvalidInput({
1462
+ issue: `idempotency key ${key} was used for another run`
1463
+ });
1464
+ }
1465
+ if (holder.claimant === claimant) return { _tag: "Claimed" };
1466
+ const rows = yield* sql`
1467
+ SELECT fingerprint, receipt_json AS "receiptJson"
1468
+ FROM control_mutations WHERE mutation_key = ${key}
1469
+ `;
1470
+ const record = rows[0];
1471
+ if (record === void 0 || record.fingerprint !== fingerprint) {
1472
+ return yield* new import_ControlError.PersistenceError({
1473
+ operation: "claim a run key",
1474
+ message: `Run key ${key} has no matching settled receipt`
1475
+ });
1476
+ }
1477
+ const receipt = yield* decodeStoredJson("control_mutations.receipt_json", import_ControlSchema.Receipt, record.receiptJson);
1478
+ return { _tag: "Raced", receipt };
1479
+ })).pipe(
1480
+ import_effect.Effect.mapError(
1481
+ (cause) => cause instanceof import_ControlError.InvalidInput || cause instanceof import_ControlError.PersistenceError ? cause : persistence("claim a run key")(cause)
1482
+ )
1483
+ );
1484
+ }),
1485
+ releaseRunKey: import_effect.Effect.fn("SqlControlRuntime.releaseRunKey")(
1486
+ (key) => writer.write(sql`
1487
+ DELETE FROM control_run_keys WHERE idempotency_key = ${key}
1488
+ `).pipe(
1489
+ import_effect.Effect.asVoid,
1490
+ import_effect.Effect.mapError(persistence("release a run key"))
1491
+ )
1492
+ ),
1493
+ grants: import_effect.Effect.fn("SqlControlRuntime.grants")(
1494
+ () => sql`
1495
+ SELECT token_id AS "tokenId", envelope_json AS "envelopeJson",
1496
+ scope, installed_at_ms AS "installedAtMs"
1497
+ FROM control_grants ORDER BY installed_at_ms, target_tag, run_id, target_id
1498
+ `.pipe(
1499
+ query("list grants"),
1500
+ import_effect.Effect.flatMap(
1501
+ (rows) => import_effect.Effect.forEach(rows, (row) => import_effect.Effect.all({
1502
+ envelope: decodeStoredJson("control_grants.envelope_json", import_ControlSchema.Envelope, row.envelopeJson),
1503
+ scope: decodeStoredValue("control_grants.scope", import_ControlSchema.GrantScope, row.scope)
1504
+ }).pipe(
1505
+ import_effect.Effect.map(({ envelope, scope }) => ({
1506
+ tokenId: row.tokenId,
1507
+ envelope,
1508
+ scope,
1509
+ installedAt: Number(row.installedAtMs)
1510
+ }))
1511
+ ))
1512
+ )
1513
+ )
1514
+ )()
1515
+ });
1516
+ return service;
1517
+ });
1518
+ };
1519
+ const layer = (options = {}) => import_effect.Layer.effect(import_ControlRuntime.ControlRuntime)(makeRuntime(options));
1520
+ const layerWithStore = (options = {}) => layer(options).pipe(import_effect.Layer.provideMerge(import_run_store.RunStore.layer));
1521
+ //# sourceMappingURL=SqlControlRuntime.js.map