@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,1597 @@
1
+ /**
2
+ * Browser-safe port between the control plane and the execution engine, with a
3
+ * deterministic in-memory implementation.
4
+ *
5
+ * `layerMemory` is the deterministic one: it models the production fence and
6
+ * approval ordering seams but keeps everything in a `Map`, so nothing it
7
+ * decides survives the process. The durable adapter is
8
+ * {@link SqlControlRuntime}, and both are held to one shared contract suite —
9
+ * see `test/ControlContract.ts`.
10
+ *
11
+ * @since 0.1.0
12
+ */
13
+
14
+ import * as Fault from "@smthrs/flow/Fault"
15
+ import type * as PersistedPlan from "@smthrs/plan/Plan"
16
+ import { Context, Crypto, Effect, Fiber, Layer, Option, Schema } from "effect"
17
+ import * as ApprovalAuthority from "./ApprovalAuthority.ts"
18
+ import type { ApprovalTarget, PlanInput } from "./Control.ts"
19
+ import {
20
+ AlreadyResolved,
21
+ ClaimLost,
22
+ type CodeDrift,
23
+ EnvelopeMismatch,
24
+ FlowNotFound,
25
+ InvalidInput,
26
+ PersistenceError,
27
+ PlanDenied,
28
+ PlanDigestMismatch,
29
+ PlanNotFound,
30
+ RunNotFound,
31
+ type Unauthorized
32
+ } from "./ControlError.ts"
33
+ import type {
34
+ Envelope,
35
+ FlowId,
36
+ GrantScope,
37
+ IdempotencyKey,
38
+ PlanCard,
39
+ PlanDecision,
40
+ PlanGraph,
41
+ PlanNode,
42
+ Principal,
43
+ Receipt,
44
+ RunId,
45
+ RunStatus,
46
+ RunSummary,
47
+ SignalPayload
48
+ } from "./ControlSchema.ts"
49
+ import { GrantScope as GrantScopeSchema, Principal as PrincipalSchema } from "./ControlSchema.ts"
50
+ import { canonicalIssue } from "./internal/issues.ts"
51
+ import {
52
+ accepted,
53
+ adoptedCode,
54
+ alreadyApplied as replayReceipt,
55
+ budgeted,
56
+ canonical,
57
+ codeDriftOf,
58
+ emptyEnvelope,
59
+ planCard,
60
+ planFingerprint,
61
+ sameEnvelope
62
+ } from "./internal/planning.ts"
63
+ import { plannable } from "./SystemFlows.ts"
64
+
65
+ /**
66
+ * Immutable ordering keys for a run page. Control launches precede engine runs.
67
+ *
68
+ * @category models
69
+ * @since 1.0.0
70
+ */
71
+ export interface RunCursor {
72
+ readonly source: 0 | 1
73
+ readonly sequence: number
74
+ readonly createdAt: number
75
+ readonly runId: RunId
76
+ }
77
+
78
+ /**
79
+ * Durable summary filters, applied before projection and executor observation.
80
+ * `limit` must be an integer from 1 through 500. Reuse a cursor with the same filters.
81
+ *
82
+ * @category models
83
+ * @since 1.0.0
84
+ */
85
+ export interface RunQuery {
86
+ readonly filters?: {
87
+ readonly flowId?: FlowId | undefined
88
+ readonly status?: RunStatus | undefined
89
+ readonly terminal?: boolean | undefined
90
+ readonly parentRunId?: RunId | undefined
91
+ readonly lineageId?: string | undefined
92
+ /** Created at or after this epoch millisecond. */
93
+ readonly since?: number | undefined
94
+ /** Created before this epoch millisecond. */
95
+ readonly until?: number | undefined
96
+ /** Only these runs; an empty list selects none. */
97
+ readonly runIds?: ReadonlyArray<RunId> | undefined
98
+ /**
99
+ * Only runs this plane launched for a principal with this `id`, and this
100
+ * `kind` when given. A run with no recorded launcher never matches.
101
+ */
102
+ readonly launchedBy?: { readonly id: string; readonly kind?: string | undefined } | undefined
103
+ } | undefined
104
+ /** Creation time first, newest or oldest; ties use the durable sequence. */
105
+ readonly order?: "newest" | "oldest" | undefined
106
+ readonly cursor?: RunCursor | undefined
107
+ readonly limit: number
108
+ }
109
+
110
+ /**
111
+ * Whether `run` was launched by the principal `launcher` names: the same `id`,
112
+ * and the same `kind` when `launcher` gives one. A run with no recorded
113
+ * launcher never matches.
114
+ *
115
+ * @category predicates
116
+ * @since 1.0.0
117
+ */
118
+ export const launchedByMatches = (
119
+ run: Pick<RunSummary, "launchedBy">,
120
+ launcher: { readonly id: string; readonly kind?: string | undefined }
121
+ ): boolean =>
122
+ run.launchedBy !== undefined && run.launchedBy.id === launcher.id &&
123
+ (launcher.kind === undefined || run.launchedBy.kind === launcher.kind)
124
+
125
+ /**
126
+ * A bounded page; the cursor names the last selected row, even if retention removed it.
127
+ *
128
+ * @category models
129
+ * @since 1.0.0
130
+ */
131
+ export interface RunPage {
132
+ readonly items: ReadonlyArray<RunSummary>
133
+ readonly nextCursor?: RunCursor | undefined
134
+ }
135
+
136
+ /**
137
+ * One page of an inventory walk. `next` is the position to pass as `after`
138
+ * for the following page; it is absent on the last page. `through` is the
139
+ * highest position the walk covers: the first page pins it to the newest
140
+ * entry at that moment, and the caller passes it back unchanged so every
141
+ * later page stops there. A walk therefore ends even while new entries keep
142
+ * arriving; they sort after `through` and belong to the next walk.
143
+ *
144
+ * @category models
145
+ * @since 1.0.0
146
+ */
147
+ export interface IdPage {
148
+ readonly ids: ReadonlyArray<string>
149
+ readonly next?: number | undefined
150
+ readonly through: number
151
+ }
152
+
153
+ /**
154
+ * An inventory page request. Positions are the store's insertion-ordered row
155
+ * key, so each page is one indexed seek. `limit` must be an integer from 1
156
+ * through 500.
157
+ *
158
+ * @category models
159
+ * @since 1.0.0
160
+ */
161
+ export interface IdPageRequest {
162
+ readonly after?: number | undefined
163
+ readonly through?: number | undefined
164
+ readonly limit: number
165
+ }
166
+
167
+ /**
168
+ * A durably admitted signal, bound at most once to one concrete wait token.
169
+ *
170
+ * @since 1.0.0
171
+ * @category models
172
+ */
173
+ export interface SignalCommand {
174
+ readonly commandId: string
175
+ readonly runId: RunId
176
+ readonly signal: SignalPayload
177
+ readonly token: string | null
178
+ readonly state: "pending" | "delivered" | "rejected" | "terminal"
179
+ /**
180
+ * Who admitted the signal, stamped at admission. The executor checks it
181
+ * against `authorizeApproval` before the signal may complete a human wait,
182
+ * and a replay after restart reads it here because no caller is present.
183
+ * Absent on commands admitted before the column existed, which therefore
184
+ * cannot answer a human wait.
185
+ */
186
+ readonly principal?: Principal | undefined
187
+ }
188
+
189
+ /**
190
+ * A stored-plan page request: plans oldest first, after the insertion position
191
+ * `after`, narrowed by flow and decision. `limit` must be an integer from 1
192
+ * through 500.
193
+ *
194
+ * @category models
195
+ * @since 1.0.0
196
+ */
197
+ export interface PlanQuery {
198
+ readonly flowId?: FlowId | undefined
199
+ readonly decision?: PlanDecision | undefined
200
+ readonly after?: number | undefined
201
+ readonly limit: number
202
+ }
203
+
204
+ /**
205
+ * One page of stored plans. `next` is the insertion position a later page
206
+ * continues after, present while more plans may match.
207
+ *
208
+ * @category models
209
+ * @since 1.0.0
210
+ */
211
+ export interface PlanPage {
212
+ readonly plans: ReadonlyArray<StoredPlan>
213
+ readonly next?: number | undefined
214
+ }
215
+
216
+ /**
217
+ * A decoded input and immutable plan stored before execution.
218
+ *
219
+ * @category models
220
+ * @since 0.1.0
221
+ */
222
+ export interface StoredPlan {
223
+ readonly card: PlanCard
224
+ readonly decodedInput: unknown
225
+ readonly decision: PlanDecision
226
+ }
227
+
228
+ /**
229
+ * The durable answer to an approval request. A decision is not inferred from
230
+ * the presence of a grant or from a boolean that also means denial.
231
+ *
232
+ * @category models
233
+ * @since 0.1.0
234
+ */
235
+ export const ApprovalDecision = Schema.Union([
236
+ Schema.Struct({ _tag: Schema.Literal("Pending") }),
237
+ Schema.Struct({
238
+ _tag: Schema.Literal("Approved"),
239
+ decisionPrincipal: PrincipalSchema,
240
+ decidedAt: Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)),
241
+ scope: GrantScopeSchema
242
+ }),
243
+ Schema.Struct({
244
+ _tag: Schema.Literal("Denied"),
245
+ decisionPrincipal: PrincipalSchema,
246
+ decidedAt: Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0))
247
+ })
248
+ ])
249
+
250
+ /**
251
+ * Durable approval state.
252
+ * @category models
253
+ * @since 1.0.0
254
+ */
255
+ export type ApprovalDecision = typeof ApprovalDecision.Type
256
+
257
+ /**
258
+ * An identity and its explicit approval decision.
259
+ * @category models
260
+ * @since 1.0.0
261
+ */
262
+ export type ApprovalToken = ApprovalDecision & {
263
+ readonly tokenId: string
264
+ readonly target: ApprovalTarget
265
+ }
266
+
267
+ /**
268
+ * A gate that still needs a decision.
269
+ * @category errors
270
+ * @since 1.0.0
271
+ */
272
+ export class ApprovalPending extends Schema.TaggedError<ApprovalPending>()("/control/ApprovalPending", {
273
+ tokenId: Schema.String
274
+ }) {
275
+ override get message(): string {
276
+ return "Approval is still pending. Wait for a decision before proceeding."
277
+ }
278
+ }
279
+
280
+ /**
281
+ * A gate that was denied, not merely resolved.
282
+ * @category errors
283
+ * @since 1.0.0
284
+ */
285
+ export class ApprovalDenied extends Schema.TaggedError<ApprovalDenied>()("/control/ApprovalDenied", {
286
+ tokenId: Schema.String,
287
+ decisionPrincipal: PrincipalSchema
288
+ }) {
289
+ override get message(): string {
290
+ return "Approval was denied. This request cannot authorize the gated work."
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Opens only an explicitly approved gate. Callers may park on ApprovalPending;
296
+ * ApprovalDenied is terminal. This reads a runtime-issued token, not an
297
+ * authentication credential, and does not itself install permissions.
298
+ * @category combinators
299
+ * @since 1.0.0
300
+ */
301
+ export const requireApproved = (
302
+ token: ApprovalToken
303
+ ): Effect.Effect<Extract<ApprovalToken, { readonly _tag: "Approved" }>, ApprovalPending | ApprovalDenied> =>
304
+ token._tag === "Approved"
305
+ ? Effect.succeed(token)
306
+ : token._tag === "Pending"
307
+ ? Effect.fail(new ApprovalPending({ tokenId: token.tokenId }))
308
+ : Effect.fail(new ApprovalDenied({ tokenId: token.tokenId, decisionPrincipal: token.decisionPrincipal }))
309
+
310
+ /**
311
+ * One bulk permission grant. The envelope is deliberately not split into
312
+ * individual capabilities at this boundary.
313
+ *
314
+ * @category models
315
+ * @since 0.1.0
316
+ */
317
+ export interface BulkGrant {
318
+ readonly tokenId: string
319
+ readonly envelope: Envelope
320
+ readonly scope: GrantScope
321
+ readonly installedAt: number
322
+ }
323
+
324
+ /**
325
+ * Result of launching an approved plan.
326
+ *
327
+ * @category models
328
+ * @since 0.1.0
329
+ */
330
+ export type LaunchResult =
331
+ | {
332
+ readonly _tag: "Started"
333
+ readonly receipt: Receipt
334
+ readonly run: RunSummary
335
+ }
336
+ | {
337
+ readonly _tag: "Parked"
338
+ readonly receipt: Receipt
339
+ }
340
+
341
+ /**
342
+ * A plan card and whether this call is the one that created it.
343
+ *
344
+ * A plan under an idempotency key the runtime has already seen answers with the
345
+ * STORED card, which is what idempotency means. `Control.plan` needs to tell
346
+ * that apart from a first ask, because it journals `control.plan.created` and
347
+ * an unconditional entry appended one creation per retry: `Channels.ingest`
348
+ * passes a key on every webhook redelivery, so a watcher of the plan partition
349
+ * replayed N creations of one plan.
350
+ *
351
+ * @category models
352
+ * @since 0.1.0
353
+ */
354
+ export interface PlanOutcome {
355
+ readonly card: PlanCard
356
+ readonly created: boolean
357
+ }
358
+
359
+ /**
360
+ * A stored idempotency-key outcome and the mutation fingerprint that produced
361
+ * it.
362
+ *
363
+ * @category models
364
+ * @since 0.1.0
365
+ */
366
+ export interface MutationRecord {
367
+ readonly fingerprint: string
368
+ readonly receipt: Receipt
369
+ }
370
+
371
+ /**
372
+ * The outcome of claiming a run mutation's idempotency key before launch.
373
+ *
374
+ * `Claimed` is this call's mandate to launch: the key row was empty or did not
375
+ * exist, and it now names this call. `Raced` is the resolution the plan verb's
376
+ * key claim gives a loser — another mutation claimed the key first, and the
377
+ * receipt it settled is the answer this call must return instead of launching
378
+ * a second run.
379
+ *
380
+ * @category models
381
+ * @since 0.1.0
382
+ */
383
+ export type RunKeyClaim =
384
+ | { readonly _tag: "Claimed" }
385
+ | { readonly _tag: "Raced"; readonly receipt: Receipt }
386
+
387
+ /**
388
+ * Flow metadata used by the memory runtime's input-decoding hook.
389
+ *
390
+ * @category models
391
+ * @since 0.1.0
392
+ */
393
+ export interface MemoryFlow {
394
+ readonly flowId: FlowId
395
+ readonly description: string
396
+ readonly deployClass: boolean
397
+ readonly envelope: Envelope
398
+ /** Executable source/metadata identity included in the approved card. */
399
+ readonly executionDigest?: string | undefined
400
+ readonly decode?: ((input: unknown) => Effect.Effect<unknown, InvalidInput>) | undefined
401
+ /**
402
+ * Projects the decoded input into the keyed node graph the card reports.
403
+ *
404
+ * Planning performs no I/O, so this is a pure function of the input: the
405
+ * host builds the graph (`@smthrs/core`'s `Graph.build`), keys it
406
+ * (`@smthrs/plan`'s compiler), and reports each node as `cached` or `run`
407
+ * against whatever step cache it holds.
408
+ */
409
+ readonly plan?:
410
+ | ((
411
+ input: unknown,
412
+ planId: string
413
+ ) => Effect.Effect<{
414
+ readonly plan: PersistedPlan.Plan
415
+ readonly statuses?: Readonly<Record<string, PlanNode["status"]>> | undefined
416
+ /** The built graph's labelled edges and declaration sites, reported outside the approval digest. */
417
+ readonly graph?: PlanGraph | undefined
418
+ }, InvalidInput>)
419
+ | undefined
420
+ }
421
+
422
+ /**
423
+ * In-memory runtime configuration.
424
+ *
425
+ * @category models
426
+ * @since 0.1.0
427
+ */
428
+ export interface MemoryOptions {
429
+ readonly flows?: ReadonlyArray<MemoryFlow> | undefined
430
+ readonly now?: (() => number) | undefined
431
+ readonly principal?: Omit<Principal, "stampedAt"> | undefined
432
+ readonly approvalAuthority?: ApprovalAuthority.Service | undefined
433
+ /** The engine version stamped on every run this runtime starts. */
434
+ readonly engineVersion?: string | undefined
435
+ }
436
+
437
+ /**
438
+ * One run that has been told to resume, and the sequence of the request.
439
+ *
440
+ * @category models
441
+ * @since 0.1.0
442
+ */
443
+ export interface PendingResume {
444
+ readonly runId: RunId
445
+ readonly sequence: number
446
+ /**
447
+ * When the delegation was recorded.
448
+ *
449
+ * A host reads it to tell a decision it has just been handed from one that
450
+ * has been standing unanswered: a run parked by a process that has since
451
+ * exited has nobody left to recognize its own park, so its delegation is
452
+ * taken up by whichever host can drive it once it has gone unanswered for
453
+ * `Ownership.heartbeatStaleAfter` (triage B-15).
454
+ */
455
+ readonly requestedAtMs: number
456
+ /**
457
+ * The journal sequence of the operator's explicit `control.run.resume`
458
+ * this delegation carries, when it carries one.
459
+ *
460
+ * An approval or a wake is background intent. An operator's resume of a
461
+ * run a live host parked is handed to that host as a delegation too, and
462
+ * this is what tells the host the operator asked: it records the
463
+ * per-release retry permission under this sequence before it drives the
464
+ * run (#2982, #3342).
465
+ */
466
+ readonly consent?: number | undefined
467
+ }
468
+
469
+ /**
470
+ * Execution-engine operations required by `ControlLive`.
471
+ *
472
+ * A production adapter must fence every owner-sensitive write, implement
473
+ * resume as join-or-claim, release claims on every waiting or terminal
474
+ * transition, and translate all conflicts into typed failures. For a new
475
+ * approval decision, authenticate the principal and call `authorizeApproval`
476
+ * before target reads or receipt replay. Then call `lookupApproval`,
477
+ * `resolveApproval` exactly once with an authority recheck, `installBulkGrant`
478
+ * only on approval, and journal the decision. Commit the decision, grant,
479
+ * journal entry, receipt, and any node resume delegation atomically. Resolution
480
+ * must not require an installed grant or a flushed journal decision.
481
+ *
482
+ * @category models
483
+ * @since 0.1.0
484
+ */
485
+ export interface Service {
486
+ /** The owning host's policy, independent of authentication and attribution. */
487
+ readonly authorizeApproval: ApprovalAuthority.Service["authorize"]
488
+ readonly plan: (input: PlanInput) => Effect.Effect<PlanOutcome, FlowNotFound | InvalidInput | PersistenceError>
489
+ readonly getPlan: (planId: string) => Effect.Effect<StoredPlan, PlanNotFound | PersistenceError>
490
+ /** Plan ids in insertion order, one bounded page at a time. */
491
+ readonly pagePlanIds: (
492
+ request: IdPageRequest
493
+ ) => Effect.Effect<IdPage, InvalidInput | PersistenceError>
494
+ /** Stored plans in insertion order, narrowed by flow and decision, one bounded page at a time. */
495
+ readonly queryPlans: (request: PlanQuery) => Effect.Effect<PlanPage, InvalidInput | PersistenceError>
496
+ readonly lookupApproval: (
497
+ target: ApprovalTarget
498
+ ) => Effect.Effect<
499
+ ApprovalToken,
500
+ PlanDigestMismatch | EnvelopeMismatch | AlreadyResolved | PlanNotFound | RunNotFound | PersistenceError
501
+ >
502
+ /**
503
+ * Creates the durable token for one in-run approval request, or returns the
504
+ * existing one.
505
+ *
506
+ * Plan tokens are created by `plan`; nothing created tokens for `Node`
507
+ * targets, so an in-run request (a parked `ask`, a permission requirement)
508
+ * could never be decided through `approve`/`deny`. Registration is
509
+ * idempotent — the executor calls it on every parked attempt — and returns
510
+ * the token with its current tagged decision so a resumed attempt can read
511
+ * the decision instead of parking again. A registered target that
512
+ * disagrees with the stored digest or envelope is refused, exactly as
513
+ * `lookupApproval` refuses it.
514
+ */
515
+ readonly registerApproval: (
516
+ target: Extract<ApprovalTarget, { readonly _tag: "Node" }>
517
+ ) => Effect.Effect<
518
+ ApprovalToken,
519
+ RunNotFound | PlanDigestMismatch | EnvelopeMismatch | PersistenceError
520
+ >
521
+ readonly installBulkGrant: (
522
+ token: ApprovalToken,
523
+ envelope: Envelope,
524
+ scope: GrantScope
525
+ ) => Effect.Effect<void, PersistenceError>
526
+ /** Records the decision exactly once. Approval scope defaults to once; the
527
+ * control boundary supplies the scope of the grant it installed. */
528
+ readonly resolveApproval: (
529
+ token: ApprovalToken,
530
+ decision: "approved" | "denied",
531
+ principal: Principal,
532
+ scope?: GrantScope | undefined
533
+ ) => Effect.Effect<void, AlreadyResolved | PersistenceError | Unauthorized>
534
+ /**
535
+ * Starts an approved plan. `principal` is the identity that asked; its `id`
536
+ * and `kind` are recorded as the run's `launchedBy`, which decides which
537
+ * readers a restricted listing or watch shows the run to.
538
+ */
539
+ readonly launch: (
540
+ planId: string,
541
+ digest: string,
542
+ envelope: Envelope,
543
+ principal?: Principal | undefined,
544
+ reservedRunId?: RunId | undefined
545
+ ) => Effect.Effect<
546
+ LaunchResult,
547
+ PlanNotFound | PlanDenied | PlanDigestMismatch | EnvelopeMismatch | ClaimLost | PersistenceError
548
+ >
549
+ readonly getRun: (runId: RunId) => Effect.Effect<RunSummary, RunNotFound | PersistenceError>
550
+ /**
551
+ * Every durable run id in insertion order, one bounded page at a time, for
552
+ * recovery and journal partition discovery. Reads keys only; use queryRuns
553
+ * for summaries.
554
+ */
555
+ readonly pageRunIds: (
556
+ request: IdPageRequest
557
+ ) => Effect.Effect<IdPage, InvalidInput | PersistenceError>
558
+ readonly queryRuns: (request: RunQuery) => Effect.Effect<RunPage, InvalidInput | PersistenceError>
559
+ readonly listFlows: Effect.Effect<
560
+ ReadonlyArray<{ readonly flowId: FlowId; readonly description: string }>,
561
+ PersistenceError
562
+ >
563
+ readonly deliverSignal: (runId: RunId, signal: SignalPayload) => Effect.Effect<void, RunNotFound | PersistenceError>
564
+ readonly admitSignal: (
565
+ commandId: string,
566
+ runId: RunId,
567
+ signal: SignalPayload,
568
+ principal?: Principal | undefined
569
+ ) => Effect.Effect<void, RunNotFound | PersistenceError>
570
+ readonly signalCommand: (commandId: string) => Effect.Effect<SignalCommand | undefined, PersistenceError>
571
+ readonly pendingSignals: Effect.Effect<ReadonlyArray<SignalCommand>, PersistenceError>
572
+ readonly bindSignal: (commandId: string, token: string) => Effect.Effect<string | null, PersistenceError>
573
+ readonly settleSignal: (
574
+ commandId: string,
575
+ state: "delivered" | "rejected" | "terminal"
576
+ ) => Effect.Effect<void, PersistenceError>
577
+ readonly deliveredSignals: (
578
+ runId: RunId
579
+ ) => Effect.Effect<ReadonlyArray<SignalPayload>, RunNotFound | PersistenceError>
580
+ /**
581
+ * Records, durably, that this run has been told to resume.
582
+ *
583
+ * The record is the delegation. A decision on an in-run approval can be
584
+ * taken by any process holding the control database, and only the process
585
+ * hosting the execution can act on it, so the intent has to outlive the call
586
+ * that made it: an in-process event bus reaches one process, and a journal
587
+ * entry is per run and needs a reader that already knows which run to read.
588
+ * The returned sequence is the cursor {@link Service.clearResume} checks, so
589
+ * a resume requested while one is being taken up is not lost with it.
590
+ *
591
+ * A terminal run is refused with `InvalidInput`: no host will ever take the
592
+ * delegation up, and recording it would leave an orphaned row every host's
593
+ * `pendingResumes` poll filters but nothing ever clears.
594
+ *
595
+ * `consent` is the journal sequence of an operator's explicit resume (see
596
+ * {@link PendingResume.consent}). A later delegation without one keeps the
597
+ * consent no host has taken up yet.
598
+ */
599
+ readonly requestResume: (
600
+ runId: RunId,
601
+ options?: { readonly consent?: number | undefined } | undefined
602
+ ) => Effect.Effect<number, RunNotFound | InvalidInput | PersistenceError>
603
+ /**
604
+ * Every outstanding resume delegation for a run that is not terminal.
605
+ *
606
+ * This is what a host polls. A settled run's delegation is not reported: no
607
+ * host will ever take it up, and reporting it forever would make an
608
+ * unbounded backlog out of a run that is finished.
609
+ */
610
+ readonly pendingResumes: Effect.Effect<ReadonlyArray<PendingResume>, PersistenceError>
611
+ /**
612
+ * Clears a delegation a host has taken up, if it is still the one it read.
613
+ *
614
+ * The sequence check is what makes the clear safe: a resume requested
615
+ * between the read and the clear has a higher sequence and survives, so the
616
+ * host takes it up on its next tick instead of losing it.
617
+ */
618
+ readonly clearResume: (runId: RunId, sequence: number) => Effect.Effect<void, PersistenceError>
619
+ readonly registerFiber: (
620
+ runId: RunId,
621
+ fiber: Fiber.Fiber<unknown, unknown>
622
+ ) => Effect.Effect<void, RunNotFound | PersistenceError>
623
+ /**
624
+ * Interrupts and awaits the local fiber with no caller-held mutation locks.
625
+ * `settle` wraps only the subsequent fenced status reconciliation, allowing
626
+ * ControlLive to commit its terminal event in the same transaction.
627
+ */
628
+ readonly interrupt: (
629
+ runId: RunId,
630
+ settle?: (
631
+ effect: Effect.Effect<RunSummary, RunNotFound | ClaimLost | PersistenceError>
632
+ ) => Effect.Effect<RunSummary, RunNotFound | ClaimLost | PersistenceError>
633
+ ) => Effect.Effect<RunSummary, RunNotFound | ClaimLost | PersistenceError>
634
+ /**
635
+ * Joins an owned run or claims a suspended run or expired dead-owner run.
636
+ *
637
+ * `scope: "launched"` restricts claims to runs this plane launched, including
638
+ * dead-owner takeovers. An unindexed run is refused with `ClaimLost`; a run
639
+ * already owned by this process is joined without changing its state.
640
+ * Both `Control.resume` and `Control.run` with a Resume input, plus steer
641
+ * wakes, pass it to preserve an engine-created run's continuation and fence.
642
+ * `scope: "any"` (also the default) is a trusted low-level runtime
643
+ * capability for hosts that can drive the claimed execution. Node approval
644
+ * uses `requestResume` delegation instead of claiming here.
645
+ */
646
+ readonly resume: (
647
+ runId: RunId,
648
+ options?: {
649
+ readonly scope?: "launched" | "any" | undefined
650
+ } | undefined
651
+ ) => Effect.Effect<RunSummary, RunNotFound | ClaimLost | PersistenceError>
652
+ /**
653
+ * `resume`, recording the flow's current execution digest and this
654
+ * runtime's engine version on the claimed run: the operator allowed it to
655
+ * resume on changed code, so later checks compare against that code. A flow
656
+ * that no longer exists, or that this host cannot run, has no code to adopt,
657
+ * so this fails `CodeDrift` before the claim and the run stays where it was.
658
+ */
659
+ readonly resumeAdopting: (
660
+ runId: RunId,
661
+ options?: { readonly scope?: "launched" | "any" | undefined } | undefined
662
+ ) => Effect.Effect<RunSummary, RunNotFound | ClaimLost | CodeDrift | PersistenceError>
663
+ /**
664
+ * The drift between the code the run recorded and the code that would
665
+ * resume it, or `undefined` when there is none. Every path that re-drives a
666
+ * parked run asks this before it claims or delegates; recovery paths that
667
+ * only settle a run do not.
668
+ */
669
+ readonly codeDrift: (runId: RunId) => Effect.Effect<CodeDrift | undefined, RunNotFound | PersistenceError>
670
+ /**
671
+ * The code identity the run executes under: its own recorded digest and
672
+ * engine version, or, for a row that recorded none (a trampoline round, or a
673
+ * fork or child the engine wrote), those of its nearest same-flow ancestor.
674
+ * `codeDrift` checks this identity, so an executor that enters the identity
675
+ * read here runs exactly the code the check admitted.
676
+ */
677
+ readonly recordedCode: (
678
+ runId: RunId
679
+ ) => Effect.Effect<Pick<RunSummary, "executionDigest" | "engineVersion">, RunNotFound | PersistenceError>
680
+ readonly claimFence: (runId: RunId) => Effect.Effect<string, RunNotFound | ClaimLost | PersistenceError>
681
+ /**
682
+ * Releases a launch the configured executor declined without changing its
683
+ * public `accepted` status.
684
+ *
685
+ * The run remains available to an external executor, but the launching
686
+ * process no longer appears to drive it. The presented fence is spent by
687
+ * the release and cannot authorize a later write.
688
+ */
689
+ readonly releasePending: (
690
+ runId: RunId,
691
+ fence: string
692
+ ) => Effect.Effect<RunSummary, RunNotFound | ClaimLost | PersistenceError>
693
+ readonly writeStatus: (
694
+ runId: RunId,
695
+ fence: string,
696
+ status: RunStatus
697
+ ) => Effect.Effect<RunSummary, RunNotFound | ClaimLost | PersistenceError>
698
+ readonly stampPrincipal: (submitted?: Principal | undefined) => Effect.Effect<Principal, PersistenceError>
699
+ readonly lookupMutation: (
700
+ key: IdempotencyKey,
701
+ fingerprint: string
702
+ ) => Effect.Effect<Receipt | undefined, PersistenceError>
703
+ readonly recordMutation: (
704
+ key: IdempotencyKey,
705
+ fingerprint: string,
706
+ receipt: Receipt
707
+ ) => Effect.Effect<void, PersistenceError>
708
+ /**
709
+ * Claims a run mutation's idempotency key before any launch side effect.
710
+ *
711
+ * This is the run verb's half of the race `control_plan_keys` closes for
712
+ * plans: a bare `lookupMutation`/`recordMutation` pair leaves two processes
713
+ * that both missed the lookup free to both launch, and the loser's run row
714
+ * outlives the `recordMutation` refusal that follows. The claim must run
715
+ * FIRST, inside the same write transaction the mutation later records its
716
+ * receipt in, so the loser's insert conflicts on the winner's committed key
717
+ * row before a run row, a claim, or a journal entry exists — and the winner's
718
+ * receipt, committed in that same transaction, is already there to read.
719
+ *
720
+ * `Raced` carries the winner's recorded receipt verbatim, which is the
721
+ * convergence the plan verb gives a losing planner (`created: false` with
722
+ * the stored card). A key claimed under another fingerprint is refused with
723
+ * `InvalidInput`, the same refusal `plan` gives a colliding key. A key row
724
+ * whose winner never recorded — possible only when the claim and the record
725
+ * were not one transaction — is a `PersistenceError`, because there is no
726
+ * honest receipt to answer with.
727
+ */
728
+ readonly claimRunKey: (
729
+ key: IdempotencyKey,
730
+ fingerprint: string
731
+ ) => Effect.Effect<RunKeyClaim, InvalidInput | PersistenceError>
732
+ /**
733
+ * Releases a claim {@link Service.claimRunKey} took, without a receipt.
734
+ *
735
+ * Exactly one path needs it: a launch that answered `Parked` created no run
736
+ * and records no receipt, so the claim must be withdrawn inside the same
737
+ * transaction or the key would stay held forever by a mutation that settled
738
+ * nothing. Every other outcome either records a receipt (the claim's whole
739
+ * point) or fails and rolls the claim back with its transaction.
740
+ */
741
+ readonly releaseRunKey: (key: IdempotencyKey) => Effect.Effect<void, PersistenceError>
742
+ readonly grants: Effect.Effect<ReadonlyArray<BulkGrant>, PersistenceError>
743
+ }
744
+
745
+ /**
746
+ * Service key for the execution-engine port.
747
+ *
748
+ * @category services
749
+ * @since 0.1.0
750
+ */
751
+ export class ControlRuntime extends Context.Service<ControlRuntime, Service>()(
752
+ "/control/ControlRuntime"
753
+ ) {}
754
+
755
+ /**
756
+ * Constructs a runtime service from an implementation record.
757
+ *
758
+ * @category constructors
759
+ * @since 0.1.0
760
+ */
761
+ export const make = (implementation: Service): Service => ControlRuntime.of(implementation)
762
+
763
+ /**
764
+ * Refuses an id page size outside 1 through 500, the `queryRuns` bound.
765
+ *
766
+ * @category validation
767
+ * @since 1.0.0
768
+ */
769
+ export const idPageLimit = (limit: number): Effect.Effect<void, InvalidInput> =>
770
+ Number.isSafeInteger(limit) && limit >= 1 && limit <= 500
771
+ ? Effect.void
772
+ : Effect.fail(new InvalidInput({ issue: "limit: must be an integer between 1 and 500" }))
773
+
774
+ /**
775
+ * One inventory page over dense insertion positions, where `idAt` resolves a
776
+ * position by direct lookup. Visits at most `limit` positions, so a page
777
+ * costs and holds one page however many entries exist.
778
+ */
779
+ const pageByPosition = (
780
+ request: IdPageRequest,
781
+ newest: number,
782
+ idAt: (position: number) => string | undefined
783
+ ): IdPage => {
784
+ const through = request.through ?? newest
785
+ const ids: Array<string> = []
786
+ let position = request.after ?? 0
787
+ let visited = 0
788
+ while (position < through && visited < request.limit) {
789
+ position += 1
790
+ visited += 1
791
+ const id = idAt(position)
792
+ if (id !== undefined) ids.push(id)
793
+ }
794
+ return position < through ? { ids, next: position, through } : { ids, through }
795
+ }
796
+
797
+ interface MutablePlan {
798
+ readonly card: PlanCard
799
+ readonly decodedInput: unknown
800
+ decision: "pending" | "approved" | "denied"
801
+ }
802
+
803
+ interface MutableToken {
804
+ readonly tokenId: string
805
+ readonly target: ApprovalTarget
806
+ decision: ApprovalDecision
807
+ }
808
+
809
+ interface MutableRun {
810
+ summary: RunSummary
811
+ fence?: string | undefined
812
+ localFence?: string | undefined
813
+ fiber?: Fiber.Fiber<unknown, unknown> | undefined
814
+ readonly sequence: number
815
+ readonly signals: Array<SignalPayload>
816
+ /** The sequence of the outstanding resume delegation, if there is one. */
817
+ pendingResume?: number | undefined
818
+ /** When that delegation was recorded. */
819
+ pendingResumeAtMs?: number | undefined
820
+ /** The operator's explicit resume that delegation carries, if any. */
821
+ pendingConsent?: number | undefined
822
+ }
823
+
824
+ // SQL persistence breaks caller reference identity through serialization. The
825
+ // memory adapter must copy at the same boundaries or its test results diverge
826
+ // from the durable implementation when a caller mutates an input or result.
827
+ const snapshot = <A>(value: A): A => structuredClone(value)
828
+
829
+ const asStored = (plan: MutablePlan): StoredPlan => ({
830
+ card: snapshot(plan.card),
831
+ decodedInput: snapshot(plan.decodedInput),
832
+ decision: plan.decision
833
+ })
834
+
835
+ // JSON tuple encoding keeps caller-chosen ids in separate fields, so a node's
836
+ // request id cannot alias another run's request or a plan id.
837
+ const approvalKey = (target: ApprovalTarget): string =>
838
+ target._tag === "Plan"
839
+ ? JSON.stringify([target._tag, target.planId])
840
+ : JSON.stringify([target._tag, target.runId, target.requestId])
841
+
842
+ const sameApprovalIdentity = (left: ApprovalTarget, right: ApprovalTarget): boolean =>
843
+ left._tag === "Plan"
844
+ ? right._tag === "Plan" && left.planId === right.planId
845
+ : right._tag === "Node" && left.runId === right.runId && left.requestId === right.requestId
846
+
847
+ const approvalToken = (token: MutableToken): ApprovalToken => ({
848
+ tokenId: token.tokenId,
849
+ target: snapshot(token.target),
850
+ ...snapshot(token.decision)
851
+ })
852
+
853
+ /**
854
+ * Deterministic in-memory runtime. It models the production fence and approval
855
+ * ordering seams but intentionally does not claim durable process survival;
856
+ * see `control-runtime-engine-integration`.
857
+ *
858
+ * @category layers
859
+ * @since 0.1.0
860
+ */
861
+ export const layerMemory = (options: MemoryOptions = {}): Layer.Layer<ControlRuntime, never, Crypto.Crypto> =>
862
+ Layer.effect(
863
+ ControlRuntime,
864
+ Effect.gen(function*() {
865
+ const crypto = yield* Crypto.Crypto
866
+ const now = options.now ?? Date.now
867
+ // The in-memory adapter stamps `memory`/`test` on a caller that names no
868
+ // principal, so its default policy delegates that identity as well as
869
+ // the local operator. Production policies never carry it.
870
+ const approvalAuthority = options.approvalAuthority ?? (yield* ApprovalAuthority.make([
871
+ {
872
+ principal: { id: "local", kind: "operator" },
873
+ scopes: ["once", "run", "remembered"],
874
+ targets: ["Plan", "Node"]
875
+ },
876
+ { principal: { id: "memory", kind: "test" }, scopes: ["once", "run", "remembered"], targets: ["Plan", "Node"] }
877
+ ]).pipe(Effect.orDie))
878
+ const authorizeApproval = approvalAuthority.authorize.bind(approvalAuthority)
879
+ const configuredFlows = options.flows ?? plannable.map((entry): MemoryFlow => ({
880
+ flowId: entry.flowId,
881
+ description: `Reserved ${entry.verb} system flow`,
882
+ deployClass: entry.deployClass,
883
+ envelope: emptyEnvelope
884
+ }))
885
+ const flows = new Map(configuredFlows.map((flow) =>
886
+ [
887
+ flow.flowId,
888
+ { ...flow, envelope: snapshot(flow.envelope) }
889
+ ] as const
890
+ ))
891
+ const plans = new Map<string, MutablePlan>()
892
+ const planKeys = new Map<IdempotencyKey, {
893
+ readonly fingerprint: string
894
+ readonly planId: string
895
+ }>()
896
+ const tokens = new Map<string, MutableToken>()
897
+ const runs = new Map<RunId, MutableRun>()
898
+ const mutations = new Map<IdempotencyKey, MutationRecord>()
899
+ const runKeys = new Map<IdempotencyKey, { readonly fingerprint: string }>()
900
+ const installedGrants: Array<BulkGrant> = []
901
+ const installedGrantKeys = new Set<string>()
902
+ let planSequence = 0
903
+ const signalCommands = new Map<string, SignalCommand>()
904
+ let pendingSignalOffset = 0
905
+ let runSequence = 0
906
+ let fenceSequence = 0
907
+ let resumeSequence = 0
908
+
909
+ const requireRun = (runId: RunId): Effect.Effect<MutableRun, RunNotFound> =>
910
+ Effect.fromOption(Option.fromNullishOr(runs.get(runId)), () => new RunNotFound({ runId }))
911
+
912
+ const updateSummary = (run: MutableRun, fields: Partial<RunSummary>): RunSummary => {
913
+ run.summary = snapshot({ ...run.summary, ...fields, updatedAt: now() })
914
+ return snapshot(run.summary)
915
+ }
916
+
917
+ const checkFence = (runId: RunId, run: MutableRun, fence: string): Effect.Effect<void, ClaimLost> =>
918
+ run.fence === undefined || run.fence !== fence
919
+ ? Effect.fail(new ClaimLost({ runId }))
920
+ : Effect.void
921
+
922
+ /** `resume`, recording the identity `adopt` answers on the claimed run when given. */
923
+ const resumeRun = <E = never>(
924
+ runId: RunId,
925
+ adopt?:
926
+ | ((run: MutableRun) => Effect.Effect<Pick<RunSummary, "executionDigest" | "engineVersion">, E>)
927
+ | undefined
928
+ ): Effect.Effect<RunSummary, RunNotFound | ClaimLost | E> =>
929
+ Effect.gen(function*() {
930
+ const run = yield* requireRun(runId)
931
+ if (
932
+ run.summary.status === "cancelled" ||
933
+ run.summary.status === "completed" ||
934
+ run.summary.status === "failed"
935
+ ) return snapshot(run.summary)
936
+ // Accepted claims are owned too; releasePending clears both fences.
937
+ if (run.fence !== undefined) {
938
+ /* v8 ignore next 3 -- one process holds this whole runtime, and it writes `fence` and `localFence` together; the peer this refuses exists only over a shared database, which is `SqlControlRuntime`'s fence */
939
+ if (run.localFence === undefined || run.fence !== run.localFence) {
940
+ return yield* new ClaimLost({ runId })
941
+ }
942
+ return snapshot(run.summary)
943
+ }
944
+ const adopted = adopt === undefined ? undefined : yield* adopt(run)
945
+ const fence = `fence-${++fenceSequence}`
946
+ run.fence = fence
947
+ run.localFence = fence
948
+ // Claiming ends the park, so it ends the record of who wrote it.
949
+ return updateSummary(run, {
950
+ ...adopted,
951
+ status: "accepted",
952
+ ownerId: "memory-owner",
953
+ parkedBy: undefined
954
+ })
955
+ })
956
+
957
+ const service = make({
958
+ authorizeApproval,
959
+ plan: Effect.fn("ControlRuntime.plan")(function*(input) {
960
+ const flow = flows.get(input.flowId)
961
+ if (flow === undefined) return yield* new FlowNotFound({ flowId: input.flowId })
962
+ const requestFingerprint = yield* Effect.try({
963
+ // Validate before cloning. Canonicalization reports a throwing
964
+ // getter at its stable path, while `structuredClone` would invoke
965
+ // the getter first and erase that safe diagnostic.
966
+ try: () => planFingerprint(input),
967
+ catch: (cause) => new InvalidInput({ issue: canonicalIssue(cause) })
968
+ })
969
+ const submitted = yield* Effect.try({
970
+ try: () => snapshot(input),
971
+ catch: (cause) => new InvalidInput({ issue: canonicalIssue(cause) })
972
+ })
973
+ if (submitted.idempotencyKey !== undefined) {
974
+ const prior = planKeys.get(submitted.idempotencyKey)
975
+ if (prior !== undefined) {
976
+ if (prior.fingerprint !== requestFingerprint) {
977
+ return yield* new InvalidInput({
978
+ issue: `idempotency key ${submitted.idempotencyKey} was used for another plan`
979
+ })
980
+ }
981
+ const stored = plans.get(prior.planId)
982
+ /* v8 ignore next -- this map never loses a plan a key names; `SqlControlRuntime` covers the storage that can */
983
+ if (stored !== undefined) return { card: snapshot(stored.card), created: false }
984
+ }
985
+ }
986
+ const decoded = yield* (flow.decode?.(submitted.input) ?? Effect.try({
987
+ try: () => {
988
+ canonical(submitted.input)
989
+ return submitted.input
990
+ },
991
+ catch: (cause) => new InvalidInput({ issue: canonicalIssue(cause) })
992
+ }))
993
+ const planId = `plan-${++planSequence}`
994
+ const handoff = flow.plan === undefined ? undefined : yield* flow.plan(decoded, planId)
995
+ const card = snapshot(
996
+ yield* planCard({
997
+ planId,
998
+ flowId: submitted.flowId,
999
+ decodedInput: decoded,
1000
+ envelope: budgeted(flow.envelope, submitted.budget),
1001
+ deployClass: flow.deployClass,
1002
+ executionDigest: flow.executionDigest,
1003
+ handoff,
1004
+ idempotencyKey: submitted.idempotencyKey
1005
+ }).pipe(Effect.provideService(Crypto.Crypto, crypto))
1006
+ )
1007
+ // Decoding, planning and hashing may yield to another keyed plan.
1008
+ // Recheck immediately before publication, with no yield between a
1009
+ // successful check and the three map writes below.
1010
+ if (submitted.idempotencyKey !== undefined) {
1011
+ const prior = planKeys.get(submitted.idempotencyKey)
1012
+ if (prior !== undefined) {
1013
+ if (prior.fingerprint !== requestFingerprint) {
1014
+ return yield* new InvalidInput({
1015
+ issue: `idempotency key ${submitted.idempotencyKey} was used for another plan`
1016
+ })
1017
+ }
1018
+ const stored = plans.get(prior.planId)
1019
+ /* v8 ignore next -- this map never loses a plan a key names; `SqlControlRuntime` covers the storage that can */
1020
+ if (stored !== undefined) return { card: snapshot(stored.card), created: false }
1021
+ }
1022
+ }
1023
+ plans.set(planId, { card, decodedInput: snapshot(decoded), decision: "pending" })
1024
+ tokens.set(approvalKey(card.approval.target), {
1025
+ tokenId: planId,
1026
+ target: snapshot(card.approval.target),
1027
+ decision: { _tag: "Pending" }
1028
+ })
1029
+ if (submitted.idempotencyKey !== undefined) {
1030
+ planKeys.set(submitted.idempotencyKey, {
1031
+ fingerprint: requestFingerprint,
1032
+ planId
1033
+ })
1034
+ }
1035
+ return { card: snapshot(card), created: true }
1036
+ }),
1037
+ getPlan: Effect.fn("ControlRuntime.getPlan")((planId) =>
1038
+ Effect.fromOption(Option.fromNullishOr(plans.get(planId)), () => new PlanNotFound({ planId })).pipe(
1039
+ Effect.map(asStored)
1040
+ )
1041
+ ),
1042
+ pagePlanIds: Effect.fn("ControlRuntime.pagePlanIds")(function*(request) {
1043
+ yield* idPageLimit(request.limit)
1044
+ return pageByPosition(
1045
+ request,
1046
+ planSequence,
1047
+ (position) => plans.has(`plan-${position}`) ? `plan-${position}` : undefined
1048
+ )
1049
+ }),
1050
+ queryPlans: Effect.fn("ControlRuntime.queryPlans")(function*(request) {
1051
+ yield* idPageLimit(request.limit)
1052
+ const matched: Array<StoredPlan> = []
1053
+ let position = request.after ?? 0
1054
+ while (position < planSequence && matched.length < request.limit) {
1055
+ position += 1
1056
+ const plan = plans.get(`plan-${position}`)
1057
+ if (
1058
+ plan !== undefined &&
1059
+ (request.flowId === undefined || plan.card.flowId === request.flowId) &&
1060
+ (request.decision === undefined || plan.decision === request.decision)
1061
+ ) matched.push(asStored(plan))
1062
+ }
1063
+ return position < planSequence ? { plans: matched, next: position } : { plans: matched }
1064
+ }),
1065
+ lookupApproval: Effect.fn("ControlRuntime.lookupApproval")(function*(target) {
1066
+ const requested = snapshot(target)
1067
+ const tokenId = requested._tag === "Plan" ? requested.planId : requested.requestId
1068
+ const token = yield* Effect.fromOption(
1069
+ Option.fromNullishOr(tokens.get(approvalKey(requested))),
1070
+ () =>
1071
+ requested._tag === "Node"
1072
+ ? new RunNotFound({ runId: requested.runId })
1073
+ : new PlanNotFound({ planId: requested.planId })
1074
+ )
1075
+ // A stored target that disagrees with its composite key is corrupted;
1076
+ // accepting only its digest and envelope would recreate the alias the
1077
+ // composite identity closes. In memory the key is DERIVED from the
1078
+ // identity, so only a caller reaching into the map can produce the
1079
+ // disagreement; `SqlControlRuntime` stores the two apart and covers
1080
+ // the same refusal against a rewritten row.
1081
+ /* v8 ignore next 6 -- unreachable while the map key is derived from the identity it is compared against */
1082
+ if (!sameApprovalIdentity(token.target, requested)) {
1083
+ return yield* new PersistenceError({
1084
+ operation: "validate an approval token",
1085
+ message: "The stored approval target does not match its identity"
1086
+ })
1087
+ }
1088
+ if (token.target.digest !== requested.digest) {
1089
+ return yield* new PlanDigestMismatch({
1090
+ planId: tokenId,
1091
+ expected: token.target.digest,
1092
+ actual: requested.digest
1093
+ })
1094
+ }
1095
+ if (!sameEnvelope(token.target.envelope, requested.envelope)) {
1096
+ return yield* new EnvelopeMismatch({
1097
+ planId: tokenId,
1098
+ expected: canonical(token.target.envelope),
1099
+ actual: canonical(requested.envelope)
1100
+ })
1101
+ }
1102
+ if (token.decision._tag !== "Pending") return yield* new AlreadyResolved({ requestId: tokenId })
1103
+ return approvalToken(token)
1104
+ }),
1105
+ registerApproval: Effect.fn("ControlRuntime.registerApproval")(function*(target) {
1106
+ const requested = snapshot(target)
1107
+ yield* requireRun(requested.runId)
1108
+ const key = approvalKey(requested)
1109
+ const existing = tokens.get(key)
1110
+ if (existing === undefined) {
1111
+ const stored: MutableToken = {
1112
+ tokenId: requested.requestId,
1113
+ target: requested,
1114
+ decision: { _tag: "Pending" }
1115
+ }
1116
+ tokens.set(key, stored)
1117
+ return approvalToken(stored)
1118
+ }
1119
+ /* v8 ignore next 6 -- as in `lookupApproval`: the map key is derived from the identity, so a stored target can only disagree with it after a reach into the map */
1120
+ if (!sameApprovalIdentity(existing.target, requested)) {
1121
+ return yield* new PersistenceError({
1122
+ operation: "validate an approval token",
1123
+ message: "The stored approval target does not match its identity"
1124
+ })
1125
+ }
1126
+ if (existing.target.digest !== requested.digest) {
1127
+ return yield* new PlanDigestMismatch({
1128
+ planId: requested.requestId,
1129
+ expected: existing.target.digest,
1130
+ actual: requested.digest
1131
+ })
1132
+ }
1133
+ if (!sameEnvelope(existing.target.envelope, requested.envelope)) {
1134
+ return yield* new EnvelopeMismatch({
1135
+ planId: requested.requestId,
1136
+ expected: canonical(existing.target.envelope),
1137
+ actual: canonical(requested.envelope)
1138
+ })
1139
+ }
1140
+ return approvalToken(existing)
1141
+ }),
1142
+ installBulkGrant: Effect.fn("ControlRuntime.installBulkGrant")((token, envelope, scope) =>
1143
+ Effect.sync(() => {
1144
+ const storedToken = snapshot(token)
1145
+ const key = approvalKey(storedToken.target)
1146
+ if (installedGrantKeys.has(key)) return
1147
+ installedGrantKeys.add(key)
1148
+ installedGrants.push({
1149
+ tokenId: storedToken.tokenId,
1150
+ envelope: snapshot(envelope),
1151
+ scope,
1152
+ installedAt: now()
1153
+ })
1154
+ })
1155
+ ),
1156
+ resolveApproval: Effect.fn("ControlRuntime.resolveApproval")(
1157
+ function*(token, decision, principal, scope = "once") {
1158
+ const requested = snapshot(token)
1159
+ const requestedPrincipal = snapshot(principal)
1160
+ const answer = yield* Schema.decodeUnknownEffect(ApprovalDecision)(
1161
+ decision === "approved"
1162
+ ? { _tag: "Approved", decisionPrincipal: requestedPrincipal, decidedAt: now(), scope }
1163
+ : { _tag: "Denied", decisionPrincipal: requestedPrincipal, decidedAt: now() }
1164
+ ).pipe(
1165
+ Effect.mapError(() =>
1166
+ new PersistenceError({
1167
+ operation: "record an approval decision",
1168
+ message: "The approval decision metadata is invalid"
1169
+ })
1170
+ )
1171
+ )
1172
+ yield* authorizeApproval({ principal: requestedPrincipal, target: requested.target, decision, scope })
1173
+ const mutable = tokens.get(approvalKey(requested.target))
1174
+ if (mutable === undefined || mutable.decision._tag !== "Pending") {
1175
+ return yield* new AlreadyResolved({ requestId: requested.tokenId })
1176
+ }
1177
+ mutable.decision = answer
1178
+ if (requested.target._tag === "Plan") {
1179
+ const plan = plans.get(requested.target.planId)
1180
+ /* v8 ignore next -- a plan token is only ever registered by `plan`, which stores the plan in the same call */
1181
+ if (plan !== undefined) plan.decision = decision
1182
+ }
1183
+ }
1184
+ ),
1185
+ launch: Effect.fn("ControlRuntime.launch")(function*(planId, requestedDigest, envelope, principal, reservedRunId) {
1186
+ const plan = yield* Effect.fromOption(
1187
+ Option.fromNullishOr(plans.get(planId)),
1188
+ () => new PlanNotFound({ planId })
1189
+ )
1190
+ if (plan.card.digest !== requestedDigest) {
1191
+ return yield* new PlanDigestMismatch({
1192
+ planId,
1193
+ expected: plan.card.digest,
1194
+ actual: requestedDigest
1195
+ })
1196
+ }
1197
+ if (!sameEnvelope(plan.card.envelope, envelope)) {
1198
+ return yield* new EnvelopeMismatch({
1199
+ planId,
1200
+ expected: canonical(plan.card.envelope),
1201
+ actual: canonical(envelope)
1202
+ })
1203
+ }
1204
+ if (plan.decision === "pending") {
1205
+ return {
1206
+ _tag: "Parked",
1207
+ receipt: {
1208
+ _tag: "Parked",
1209
+ receiptId: `launch:${planId}`,
1210
+ planId,
1211
+ status: "waiting-approval"
1212
+ }
1213
+ }
1214
+ }
1215
+ if (plan.decision !== "approved") {
1216
+ return yield* new PlanDenied({ planId })
1217
+ }
1218
+ const sequence = ++runSequence
1219
+ const runId = reservedRunId ?? `run-${sequence}`
1220
+ if (runs.has(runId)) return yield* new PersistenceError({ operation: "reserve a run", message: "Run identity already exists" })
1221
+ const fence = `fence-${++fenceSequence}`
1222
+ const timestamp = now()
1223
+ const summary: RunSummary = {
1224
+ runId,
1225
+ flowId: plan.card.flowId,
1226
+ status: "accepted",
1227
+ planId,
1228
+ planDigest: plan.card.digest,
1229
+ ...(plan.card.executionDigest === undefined ? {} : { executionDigest: plan.card.executionDigest }),
1230
+ ...(options.engineVersion === undefined ? {} : { engineVersion: options.engineVersion }),
1231
+ ownerId: "memory-owner",
1232
+ ...(principal === undefined ? {} : { launchedBy: { id: principal.id, kind: principal.kind } }),
1233
+ ...(plan.card.envelope.budget.deadline === undefined
1234
+ ? {}
1235
+ : { deadlineAt: timestamp + plan.card.envelope.budget.deadline }),
1236
+ createdAt: timestamp,
1237
+ updatedAt: timestamp
1238
+ }
1239
+ runs.set(runId, {
1240
+ summary: snapshot(summary),
1241
+ fence,
1242
+ localFence: fence,
1243
+ sequence: runSequence,
1244
+ signals: []
1245
+ })
1246
+ return {
1247
+ _tag: "Started",
1248
+ receipt: accepted(`launch:${planId}:${runId}`, runId),
1249
+ run: snapshot(summary)
1250
+ }
1251
+ }),
1252
+ getRun: Effect.fn("ControlRuntime.getRun")((runId) =>
1253
+ Effect.map(requireRun(runId), (run) => snapshot(run.summary))
1254
+ ),
1255
+ pageRunIds: Effect.fn("ControlRuntime.pageRunIds")(function*(request) {
1256
+ yield* idPageLimit(request.limit)
1257
+ return pageByPosition(
1258
+ request,
1259
+ runSequence,
1260
+ (position) => runs.has(`run-${position}`) ? `run-${position}` : undefined
1261
+ )
1262
+ }),
1263
+ queryRuns: Effect.fn("ControlRuntime.queryRuns")(function*(request) {
1264
+ if (!Number.isSafeInteger(request.limit) || request.limit < 1 || request.limit > 500) {
1265
+ return yield* new InvalidInput({ issue: "limit: must be an integer between 1 and 500" })
1266
+ }
1267
+ const selected: Array<MutableRun> = []
1268
+ const filters = request.filters
1269
+ const newest = request.order === "newest"
1270
+ const oldest = request.order === "oldest"
1271
+ const candidates = newest
1272
+ ? Array.from(runs.values()).sort((a, b) =>
1273
+ b.summary.createdAt - a.summary.createdAt || b.sequence - a.sequence
1274
+ )
1275
+ : oldest
1276
+ ? Array.from(runs.values()).sort((a, b) =>
1277
+ a.summary.createdAt - b.summary.createdAt || a.sequence - b.sequence
1278
+ )
1279
+ : runs.values()
1280
+ for (const run of candidates) {
1281
+ const after = request.cursor
1282
+ if (
1283
+ after !== undefined && (after.source !== 0 || (newest
1284
+ ? run.summary.createdAt > after.createdAt ||
1285
+ (run.summary.createdAt === after.createdAt && run.sequence >= after.sequence)
1286
+ : oldest
1287
+ ? run.summary.createdAt < after.createdAt ||
1288
+ (run.summary.createdAt === after.createdAt && run.sequence <= after.sequence)
1289
+ : run.sequence <= after.sequence))
1290
+ ) continue
1291
+ const summary = run.summary
1292
+ if (filters?.since !== undefined && summary.createdAt < filters.since) continue
1293
+ if (filters?.until !== undefined && summary.createdAt >= filters.until) continue
1294
+ if (filters?.runIds !== undefined && !filters.runIds.includes(summary.runId)) continue
1295
+ if (filters?.flowId !== undefined && summary.flowId !== filters.flowId) continue
1296
+ if (filters?.status !== undefined && summary.status !== filters.status) continue
1297
+ if (
1298
+ filters?.terminal !== undefined &&
1299
+ ["completed", "failed", "cancelled"].includes(summary.status) !== filters.terminal
1300
+ ) continue
1301
+ if (filters?.parentRunId !== undefined && summary.parentRunId !== filters.parentRunId) continue
1302
+ if (filters?.lineageId !== undefined && summary.lineageId !== filters.lineageId) continue
1303
+ if (filters?.launchedBy !== undefined && !launchedByMatches(summary, filters.launchedBy)) continue
1304
+ selected.push(run)
1305
+ if (selected.length > request.limit) break
1306
+ }
1307
+ const page = selected.slice(0, request.limit)
1308
+ const last = page.at(-1)
1309
+ return {
1310
+ items: page.map((run) => snapshot(run.summary)),
1311
+ ...(selected.length <= request.limit || last === undefined ? {} : {
1312
+ nextCursor: {
1313
+ source: 0 as const,
1314
+ sequence: last.sequence,
1315
+ createdAt: last.summary.createdAt,
1316
+ runId: last.summary.runId
1317
+ }
1318
+ })
1319
+ }
1320
+ }),
1321
+ listFlows: Effect.fn("ControlRuntime.listFlows")(() =>
1322
+ Effect.sync(() =>
1323
+ Array.from(flows.values(), (flow) => ({
1324
+ flowId: flow.flowId,
1325
+ description: flow.description
1326
+ }))
1327
+ )
1328
+ )(),
1329
+ deliverSignal: Effect.fn("ControlRuntime.deliverSignal")((runId, signal) =>
1330
+ Effect.tap(requireRun(runId), (run) => Effect.sync(() => void run.signals.push(snapshot(signal))))
1331
+ ),
1332
+ admitSignal: Effect.fn("ControlRuntime.admitSignal")(function*(commandId, runId, signal, principal) {
1333
+ yield* requireRun(runId)
1334
+ if (!signalCommands.has(commandId)) {
1335
+ signalCommands.set(
1336
+ commandId,
1337
+ snapshot({
1338
+ commandId,
1339
+ runId,
1340
+ signal,
1341
+ token: null,
1342
+ state: "pending",
1343
+ ...(principal === undefined ? {} : { principal })
1344
+ })
1345
+ )
1346
+ }
1347
+ }),
1348
+ signalCommand: (commandId) => Effect.sync(() => snapshot(signalCommands.get(commandId))),
1349
+ pendingSignals: Effect.sync(() => {
1350
+ const pending = Array.from(signalCommands.values()).filter((command) => command.state === "pending")
1351
+ if (pendingSignalOffset >= pending.length) pendingSignalOffset = 0
1352
+ const page = pending.slice(pendingSignalOffset, pendingSignalOffset + 100)
1353
+ pendingSignalOffset += page.length
1354
+ return snapshot(page)
1355
+ }),
1356
+ bindSignal: (commandId, token) =>
1357
+ Effect.gen(function*() {
1358
+ const command = signalCommands.get(commandId)
1359
+ if (command === undefined) {
1360
+ return yield* new PersistenceError({
1361
+ operation: "bind signal",
1362
+ message: `No pending signal command ${commandId}`
1363
+ })
1364
+ }
1365
+ if (command.state !== "pending") return command.token
1366
+ if (
1367
+ command.token === null &&
1368
+ Array.from(signalCommands.values()).some((other) =>
1369
+ other.commandId !== commandId && other.token === token
1370
+ )
1371
+ ) return null
1372
+ const bound = command.token ?? token
1373
+ signalCommands.set(commandId, { ...command, token: bound })
1374
+ return bound
1375
+ }),
1376
+ settleSignal: (commandId, state) =>
1377
+ Effect.sync(() => {
1378
+ const command = signalCommands.get(commandId)
1379
+ if (command !== undefined && command.state === "pending") {
1380
+ signalCommands.set(commandId, { ...command, state })
1381
+ }
1382
+ }),
1383
+ deliveredSignals: Effect.fn("ControlRuntime.deliveredSignals")((runId) =>
1384
+ Effect.map(
1385
+ requireRun(runId),
1386
+ (run) =>
1387
+ snapshot([
1388
+ ...run.signals,
1389
+ ...Array.from(signalCommands.values()).filter((command) =>
1390
+ command.runId === runId && command.state !== "rejected"
1391
+ ).map((command) => command.signal)
1392
+ ])
1393
+ )
1394
+ ),
1395
+ requestResume: Effect.fn("ControlRuntime.requestResume")(function*(runId, options) {
1396
+ const run = yield* requireRun(runId)
1397
+ if (
1398
+ run.summary.status === "cancelled" ||
1399
+ run.summary.status === "completed" ||
1400
+ run.summary.status === "failed"
1401
+ ) {
1402
+ return yield* new InvalidInput({
1403
+ issue: `run ${runId} is ${run.summary.status} and cannot take a resume`
1404
+ })
1405
+ }
1406
+ const sequence = ++resumeSequence
1407
+ run.pendingResume = sequence
1408
+ run.pendingResumeAtMs = now()
1409
+ run.pendingConsent = options?.consent ?? run.pendingConsent
1410
+ updateSummary(run, { pendingResume: sequence })
1411
+ return sequence
1412
+ }),
1413
+ pendingResumes: Effect.sync(() =>
1414
+ Array.from(runs.entries()).flatMap(([runId, run]) =>
1415
+ run.pendingResume === undefined || run.pendingResumeAtMs === undefined ||
1416
+ run.summary.status === "cancelled" ||
1417
+ run.summary.status === "completed" || run.summary.status === "failed"
1418
+ ? []
1419
+ : [{
1420
+ runId,
1421
+ sequence: run.pendingResume,
1422
+ requestedAtMs: run.pendingResumeAtMs,
1423
+ ...(run.pendingConsent === undefined ? {} : { consent: run.pendingConsent })
1424
+ }]
1425
+ )
1426
+ ),
1427
+ clearResume: Effect.fn("ControlRuntime.clearResume")((runId, sequence) =>
1428
+ Effect.sync(() => {
1429
+ const run = runs.get(runId)
1430
+ if (run === undefined || run.pendingResume !== sequence) return
1431
+ run.pendingResume = undefined
1432
+ run.pendingResumeAtMs = undefined
1433
+ run.pendingConsent = undefined
1434
+ updateSummary(run, { pendingResume: undefined })
1435
+ })
1436
+ ),
1437
+ registerFiber: Effect.fn("ControlRuntime.registerFiber")((runId, fiber) =>
1438
+ Effect.tap(requireRun(runId), (run) =>
1439
+ Effect.sync(() => {
1440
+ run.fiber = fiber
1441
+ fiber.addObserver(() => {
1442
+ if (run.fiber === fiber) run.fiber = undefined
1443
+ })
1444
+ }))
1445
+ ),
1446
+ interrupt: Effect.fn("ControlRuntime.interrupt")(function*(runId, settle = (effect) => effect) {
1447
+ const run = yield* requireRun(runId)
1448
+ // Terminal first, as `resume` does: a settled run released its fence,
1449
+ // and answering `ClaimLost` there would name the wrong problem.
1450
+ if (
1451
+ run.summary.status === "cancelled" ||
1452
+ run.summary.status === "completed" ||
1453
+ run.summary.status === "failed"
1454
+ ) return snapshot(run.summary)
1455
+ if (run.localFence === undefined) return yield* new ClaimLost({ runId })
1456
+ const fence = run.localFence
1457
+ yield* checkFence(runId, run, fence)
1458
+ if (run.fiber !== undefined) yield* Fiber.interrupt(run.fiber)
1459
+ return yield* settle(Effect.gen(function*() {
1460
+ // Cleanup may settle the run or release and replace its owner.
1461
+ if (
1462
+ run.summary.status === "cancelled" ||
1463
+ run.summary.status === "completed" ||
1464
+ run.summary.status === "failed"
1465
+ ) return snapshot(run.summary)
1466
+ yield* checkFence(runId, run, fence)
1467
+ run.fence = undefined
1468
+ run.localFence = undefined
1469
+ return updateSummary(run, { status: "cancelled", ownerId: undefined, parkedBy: undefined })
1470
+ }))
1471
+ }),
1472
+ codeDrift: Effect.fn("ControlRuntime.codeDrift")(function*(runId) {
1473
+ const run = yield* requireRun(runId)
1474
+ return codeDriftOf(run.summary, flows.get(run.summary.flowId), options.engineVersion)
1475
+ }),
1476
+ recordedCode: Effect.fn("ControlRuntime.recordedCode")(function*(runId) {
1477
+ const { executionDigest, engineVersion } = (yield* requireRun(runId)).summary
1478
+ return { executionDigest, engineVersion }
1479
+ }),
1480
+ resume: Effect.fn("ControlRuntime.resume")((runId) => resumeRun(runId)),
1481
+ // `plan` refuses a flow this catalog does not hold, and the catalog is
1482
+ // fixed at construction, so a run's flow is never gone here and there
1483
+ // is always code to adopt. `SqlControlRuntime` reads a catalog that can
1484
+ // lose a flow, and refuses that case before the claim.
1485
+ resumeAdopting: Effect.fn("ControlRuntime.resumeAdopting")((runId) =>
1486
+ resumeRun(
1487
+ runId,
1488
+ (run) => Effect.succeed(adoptedCode(run.summary, flows.get(run.summary.flowId), options.engineVersion))
1489
+ )
1490
+ ),
1491
+ claimFence: Effect.fn("ControlRuntime.claimFence")(function*(runId) {
1492
+ const run = yield* requireRun(runId)
1493
+ if (run.localFence === undefined) return yield* new ClaimLost({ runId })
1494
+ return run.localFence
1495
+ }),
1496
+ releasePending: Effect.fn("ControlRuntime.releasePending")(function*(runId, fence) {
1497
+ const run = yield* requireRun(runId)
1498
+ yield* checkFence(runId, run, fence)
1499
+ run.fence = undefined
1500
+ run.localFence = undefined
1501
+ return updateSummary(run, {
1502
+ status: "accepted",
1503
+ ownerId: undefined,
1504
+ parkedBy: undefined
1505
+ })
1506
+ }),
1507
+ writeStatus: Effect.fn("ControlRuntime.writeStatus")(function*(runId, fence, status) {
1508
+ const run = yield* requireRun(runId)
1509
+ yield* checkFence(runId, run, fence)
1510
+ // Ownership is released by any status that is not being driven, so the
1511
+ // fence that wrote a terminal or parked status is spent by writing it.
1512
+ if (status === "accepted" || status === "running") {
1513
+ return updateSummary(run, { status, parkedBy: undefined })
1514
+ }
1515
+ run.fence = undefined
1516
+ run.localFence = undefined
1517
+ // The spent fence is kept on a park, and only on a park: it is the
1518
+ // only thing left on the row that says which host parked it.
1519
+ return updateSummary(run, {
1520
+ status,
1521
+ ownerId: undefined,
1522
+ parkedBy: status === "parked" || status === "waiting-approval" ? fence : undefined
1523
+ })
1524
+ }),
1525
+ // Precedence matches `SqlControlRuntime`: the submitted identity is
1526
+ // the one a server authenticated, and the configured one is this
1527
+ // composition's fallback for a caller that named none.
1528
+ stampPrincipal: Effect.fn("ControlRuntime.stampPrincipal")((submitted) =>
1529
+ Effect.sync(() => ({
1530
+ id: submitted?.id ?? options.principal?.id ?? "memory",
1531
+ kind: submitted?.kind ?? options.principal?.kind ?? "test",
1532
+ stampedAt: now()
1533
+ }))
1534
+ ),
1535
+ lookupMutation: Effect.fn("ControlRuntime.lookupMutation")((key, fingerprint) =>
1536
+ Effect.sync(() => {
1537
+ const record = mutations.get(key)
1538
+ if (record === undefined) return undefined
1539
+ return record.fingerprint === fingerprint
1540
+ ? replayReceipt(key, record.receipt)
1541
+ : { _tag: "Conflict", message: `idempotency key ${key} was used for another mutation` }
1542
+ })
1543
+ ),
1544
+ recordMutation: Effect.fn("ControlRuntime.recordMutation")((key, fingerprint, receipt) =>
1545
+ Effect.gen(function*() {
1546
+ const prior = mutations.get(key)
1547
+ if (
1548
+ prior !== undefined &&
1549
+ (prior.fingerprint !== fingerprint || canonical(prior.receipt) !== canonical(receipt))
1550
+ ) {
1551
+ return yield* Effect.fail(
1552
+ new PersistenceError({
1553
+ operation: "record a mutation",
1554
+ message: `Idempotency key ${key} was already settled by another mutation`
1555
+ })
1556
+ )
1557
+ }
1558
+ mutations.set(key, { fingerprint, receipt: snapshot(receipt) })
1559
+ })
1560
+ ),
1561
+ claimRunKey: Effect.fn("ControlRuntime.claimRunKey")(function*(key, fingerprint) {
1562
+ const holder = runKeys.get(key)
1563
+ if (holder !== undefined) {
1564
+ if (holder.fingerprint !== fingerprint) {
1565
+ return yield* new InvalidInput({
1566
+ issue: `idempotency key ${key} was used for another run`
1567
+ })
1568
+ }
1569
+ const record = mutations.get(key)
1570
+ if (record === undefined) {
1571
+ // One process owns this runtime, so reaching here takes a caller
1572
+ // that claimed and never recorded: the cross-process winner this
1573
+ // branch really models commits both in one transaction, which is
1574
+ // `SqlControlRuntime`'s seam.
1575
+ return yield* new PersistenceError({
1576
+ operation: "read a mutation",
1577
+ message: `run key ${key} was claimed by a mutation that recorded no receipt`
1578
+ })
1579
+ }
1580
+ return { _tag: "Raced" as const, receipt: snapshot(record.receipt) }
1581
+ }
1582
+ runKeys.set(key, { fingerprint })
1583
+ return { _tag: "Claimed" as const }
1584
+ }),
1585
+ releaseRunKey: Effect.fn("ControlRuntime.releaseRunKey")((key) =>
1586
+ Effect.sync(() => {
1587
+ runKeys.delete(key)
1588
+ })
1589
+ ),
1590
+ grants: Effect.fn("ControlRuntime.grants")(() => Effect.sync(() => snapshot(installedGrants)))()
1591
+ })
1592
+ return service
1593
+ })
1594
+ )
1595
+
1596
+ Fault.register("/control/ApprovalPending", "wait")
1597
+ Fault.register("/control/ApprovalDenied", "user")