@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,1585 @@
1
+ /**
2
+ * In-process Control implementation over `ControlRuntime`, the flow
3
+ * registry, and the append-only journal.
4
+ *
5
+ * @since 0.1.0
6
+ */
7
+ import * as Sha256 from "@smthrs/crypto/Sha256";
8
+ import * as Fault from "@smthrs/flow/Fault";
9
+ import { Journal, JournalEvent } from "@smthrs/journal";
10
+ import { NotificationQueue } from "@smthrs/notifications";
11
+ import * as SteerPayload from "@smthrs/notifications/SteerPayload";
12
+ import { Registry } from "@smthrs/registry";
13
+ import { inputDocument } from "@smthrs/registry/Descriptor";
14
+ import { Cause, Deferred, Effect, Exit, Layer, Option, Schema, Semaphore, Stream } from "effect";
15
+ import * as Cancellation from "./Cancellation.js";
16
+ import { Control } from "./Control.js";
17
+ import { ClaimLost, InvalidInput, NoMatchingWait, PersistenceError, RunNotFound, Unauthorized, Unavailable } from "./ControlError.js";
18
+ import { ControlExecutor } from "./ControlExecutor.js";
19
+ import * as ControlFacts from "./ControlFacts.js";
20
+ import { ControlRuntime, launchedByMatches } from "./ControlRuntime.js";
21
+ import { ApprovalInputSchema, defaultPageSize, ListRequest, maxPageSize, Principal, ReasonedMutationInputSchema, ResumeInputSchema, RunInputSchema, SignalInputSchema, SteerInputSchema, steerItem, WatchCursor } from "./ControlSchema.js";
22
+ import * as DispatchReader from "./DispatchReader.js";
23
+ import { schemaIssuePath } from "./internal/issues.js";
24
+ import * as MutationBoundary from "./internal/MutationBoundary.js";
25
+ import { alreadyApplied, canonical, mutationKey } from "./internal/planning.js";
26
+ import * as Lineage from "./Lineage.js";
27
+ import * as Steering from "./Steering.js";
28
+ const sourceId = JournalEvent.SourceId.make("/control");
29
+ const snapshotPageSize = 1024;
30
+ const snapshotPartitionConcurrency = 8;
31
+ /** Partition ids a global watch reads per keyed inventory page. */
32
+ const partitionPageSize = 100;
33
+ const unavailable = (feature) => new Unavailable({ feature, ticket: "control-runtime-engine-integration" });
34
+ /**
35
+ * What a failed journal read during `watch` answers.
36
+ *
37
+ * A closed journal means this composition has no journal to watch, so the
38
+ * feature is unavailable. Any other code is a storage failure and keeps its
39
+ * cause, so the operator sees what the journal reported.
40
+ */
41
+ const watchReadFailed = (cause) => cause.code === "journal_closed"
42
+ ? unavailable("watch")
43
+ : new PersistenceError({
44
+ operation: "watch",
45
+ message: `Reading the control journal failed (${cause.code})`,
46
+ cause
47
+ });
48
+ const accepted = (key, runId) => runId === undefined
49
+ ? { _tag: "Accepted", receiptId: key }
50
+ : { _tag: "Accepted", receiptId: key, runId };
51
+ const terminal = (status) => status === "cancelled" || status === "completed" || status === "failed";
52
+ /**
53
+ * Whether a status means a process is holding the run right now.
54
+ *
55
+ * `accepted` is what a claim writes, and nothing rewrites it until the run
56
+ * settles: only `Control.run` promotes a run to `running`, and only when its
57
+ * own executor took the launch. A run restarted by `Control.resume` or by an
58
+ * approval therefore spends its whole second life `accepted`. Both statuses
59
+ * project onto the store's `running` (`SqlControlRuntime`'s `storeStatus`), so
60
+ * a lost claim against either one means a live peer owns the row — which
61
+ * release policy 5.1 answers `ClaimLost`. Asking for the literal `running` alone
62
+ * answered `Accepted` for a peer's accepted run and hid the peer.
63
+ */
64
+ const live = (status) => status === "running" || status === "accepted";
65
+ const terminalOrAccepted = (key, run) => terminal(run.status)
66
+ ? { _tag: "Terminal", runId: run.runId, status: run.status }
67
+ : accepted(key, run.runId);
68
+ /**
69
+ * The two paths a SERVER stamps a principal onto, and the only two an
70
+ * idempotency fingerprint may ignore.
71
+ *
72
+ * `ControlServer` overwrites `input.principal` and `input.message.principal`
73
+ * with the identity it authenticated, and the stamp carries a wall clock, so
74
+ * keeping either made the second `smithers cancel` of one run look like a
75
+ * different mutation under the same key: a bearer-authenticated retry answered
76
+ * `Conflict` instead of the cancel's own receipt.
77
+ *
78
+ * Nothing else named `principal` is stamped. The previous replacer dropped the
79
+ * key at EVERY depth, so two signals whose payloads differed only in a nested
80
+ * `principal` collided under one key and the second payload was never
81
+ * delivered.
82
+ */
83
+ const withoutStampedPrincipal = (input) => {
84
+ /* v8 ignore next -- every caller passes a mutation the boundary already decoded into a struct; the guard keeps the helper total for a reader */
85
+ if (input === null || typeof input !== "object")
86
+ return input;
87
+ const { principal: _principal, ...rest } = input;
88
+ const message = rest["message"];
89
+ if (message === null || typeof message !== "object")
90
+ return rest;
91
+ const { principal: _messagePrincipal, ...messageRest } = message;
92
+ return { ...rest, message: messageRest };
93
+ };
94
+ /**
95
+ * What an idempotency key is bound to: one actor's stated intent.
96
+ *
97
+ * The input has already crossed the inert boundary. The principal's stable id
98
+ * and kind remain in the document while its server clock is omitted, and the
99
+ * canonical bytes are reduced to one fixed-size durable digest.
100
+ */
101
+ const fingerprint = (operation, principal, input) => `control-mutation:v2:${Sha256.digestSync(canonical({
102
+ operation,
103
+ actor: { id: principal.id, kind: principal.kind },
104
+ intent: withoutStampedPrincipal(input)
105
+ }))}`;
106
+ const json = (value) => JSON.parse(JSON.stringify(value));
107
+ /**
108
+ * The trigger provenance an admitted plan input declared, verbatim.
109
+ *
110
+ * The control plane does not know the record's shape: the flow's own input
111
+ * schema bounded it at plan time (`flows/coding/dispatch.ts` `MessageTrigger`
112
+ * is the first). Admission carries it onto the accepted record so a journal
113
+ * reader — the Steps view's trigger row (#2115) — renders what was recorded,
114
+ * never what it inferred from a prompt. Anything that is not a record is
115
+ * dropped rather than carried.
116
+ */
117
+ const declaredTrigger = (decodedInput) => {
118
+ if (decodedInput === null || typeof decodedInput !== "object" || Array.isArray(decodedInput))
119
+ return undefined;
120
+ const held = decodedInput["trigger"];
121
+ return held !== null && typeof held === "object" && !Array.isArray(held) ? held : undefined;
122
+ };
123
+ const invalid = (issue) => new InvalidInput({ issue });
124
+ const AttributedApprovalInput = Schema.Struct({
125
+ ...ApprovalInputSchema.fields,
126
+ principal: Schema.optional(Principal)
127
+ });
128
+ const AttributedReasonedMutationInput = Schema.Struct({
129
+ ...ReasonedMutationInputSchema.fields,
130
+ principal: Schema.optional(Principal)
131
+ });
132
+ const AttributedResumeInput = Schema.Struct({
133
+ ...ResumeInputSchema.fields,
134
+ principal: Schema.optional(Principal)
135
+ });
136
+ const AttributedRunInput = Schema.Union([
137
+ Schema.Struct({ ...RunInputSchema.members[0].fields, principal: Schema.optional(Principal), reservedRunId: Schema.optional(Schema.NonEmptyString) }),
138
+ Schema.Struct({ ...RunInputSchema.members[1].fields, principal: Schema.optional(Principal) })
139
+ ]);
140
+ const AttributedSignalInput = Schema.Struct({
141
+ ...SignalInputSchema.fields,
142
+ principal: Schema.optional(Principal)
143
+ });
144
+ // The surrogate scan restates, at the point the DURABLE KEY is formed, what
145
+ // `MutationBoundary.admit` already refused: a lone surrogate is not a string
146
+ // SQLite and JSON round-trip identically, and this value is a primary key. The
147
+ // two refusing arms are therefore unreachable through every caller, and stay
148
+ // as the local invariant rather than as a check somebody may delete upstream.
149
+ const validIdempotencyKey = (value) => {
150
+ if (value.length === 0 || value.length > 1024 || value.includes("\0"))
151
+ return false;
152
+ for (let index = 0; index < value.length; index++) {
153
+ const unit = value.charCodeAt(index);
154
+ if (unit >= 0xd800 && unit <= 0xdbff) {
155
+ const low = value.charCodeAt(++index);
156
+ /* v8 ignore next -- an unpaired high surrogate is refused by the mutation boundary first */
157
+ if (!(low >= 0xdc00 && low <= 0xdfff))
158
+ return false;
159
+ continue;
160
+ }
161
+ /* v8 ignore next -- a lone low surrogate is refused by the mutation boundary first */
162
+ if (unit >= 0xdc00 && unit <= 0xdfff)
163
+ return false;
164
+ }
165
+ return true;
166
+ };
167
+ /** Admits, schema-decodes, and detaches one mutation before its first wait. */
168
+ const snapshotMutation = (operation, decode, input) => Effect.suspend(() => {
169
+ const admitted = MutationBoundary.admit(input);
170
+ if (!admitted.ok)
171
+ return Effect.fail(invalid(`${operation}: ${admitted.complaint}`));
172
+ return decode(admitted.value).pipe(Effect.mapError((error) => invalid(`${operation}: invalid mutation at ${schemaIssuePath(error)}`)), Effect.flatMap((snapshot) => validIdempotencyKey(snapshot.idempotencyKey)
173
+ ? Effect.succeed(snapshot)
174
+ : Effect.fail(invalid(`${operation}.idempotencyKey: must be 1 to 1024 well-formed characters`))));
175
+ });
176
+ const snapshotApproval = (operation, input) => snapshotMutation(operation, Schema.decodeUnknownEffect(AttributedApprovalInput), input);
177
+ const snapshotReasonedMutation = (operation, input) => snapshotMutation(operation, Schema.decodeUnknownEffect(AttributedReasonedMutationInput), input);
178
+ const snapshotResume = (input) => snapshotMutation("resume", Schema.decodeUnknownEffect(AttributedResumeInput), input);
179
+ const snapshotRun = (input) => snapshotMutation("run", Schema.decodeUnknownEffect(AttributedRunInput), input);
180
+ const snapshotSignal = (input) => snapshotMutation("signal", Schema.decodeUnknownEffect(AttributedSignalInput), input);
181
+ const snapshotSteer = (input) => snapshotMutation("steer", Schema.decodeUnknownEffect(SteerInputSchema), input);
182
+ /**
183
+ * Refuses a page size or cursor that cannot make progress.
184
+ *
185
+ * A `limit` of zero, a negative or fractional one, `NaN`, and `Infinity` all
186
+ * used to answer `{ items: [], nextCursor: String(start) }`, which is a cursor
187
+ * a client loops on forever; an unparsable cursor silently restarted at the
188
+ * first page. Both are caller mistakes, and a control plane that answers a
189
+ * mistake with a plausible-looking page is the partial behaviour rc.0 forbids.
190
+ * `ControlSchema.PageLimit` refuses the same sizes on the wire; this is the
191
+ * in-process half, which no schema decodes.
192
+ */
193
+ const pageBounds = (cursor, limit) => {
194
+ if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 1 || limit > maxPageSize)) {
195
+ return Effect.fail(invalid(`limit: must be an integer between 1 and ${maxPageSize}, received ${String(limit)}`));
196
+ }
197
+ if (cursor === undefined)
198
+ return Effect.succeed({ start: 0, size: limit ?? defaultPageSize });
199
+ const start = Number(cursor);
200
+ return Number.isSafeInteger(start) && start >= 0
201
+ ? Effect.succeed({ start, size: limit ?? defaultPageSize })
202
+ : Effect.fail(invalid(`cursor: must be a cursor this listing returned, received ${JSON.stringify(cursor)}`));
203
+ };
204
+ const cursorNatural = Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(Number.MAX_SAFE_INTEGER));
205
+ const runCursor = Schema.fromJsonString(Schema.Struct({
206
+ version: Schema.Literal(1),
207
+ filters: Schema.String,
208
+ source: Schema.Union([Schema.Literal(0), Schema.Literal(1)]),
209
+ sequence: cursorNatural,
210
+ createdAt: cursorNatural,
211
+ runId: Schema.NonEmptyString
212
+ }));
213
+ const page = (values, bounds) => {
214
+ const items = values.slice(bounds.start, bounds.start + bounds.size);
215
+ const next = bounds.start + items.length;
216
+ return next < values.length ? { items, nextCursor: String(next) } : { items };
217
+ };
218
+ const eventFromEntry = (entry) => ({
219
+ sequence: entry.seq,
220
+ kind: entry.eventType,
221
+ runId: entry.runId,
222
+ occurredAt: entry.emittedAtMs,
223
+ payload: entry.payload
224
+ });
225
+ /**
226
+ * Live in-process Control layer.
227
+ *
228
+ * Writes delegate to `ControlRuntime`; journal events are observational
229
+ * records. `watch` only replays and follows committed journal entries.
230
+ *
231
+ * @category layers
232
+ * @since 0.1.0
233
+ */
234
+ export const layer = Layer.effect(Control, Effect.gen(function* () {
235
+ const runtime = yield* ControlRuntime;
236
+ const journal = yield* Journal.Journal;
237
+ const notifications = yield* NotificationQueue.NotificationQueue;
238
+ const registry = yield* Registry.Registry;
239
+ const executor = yield* Effect.serviceOption(ControlExecutor);
240
+ // Optional like the executor: a composition without a trigger store still
241
+ // plans, runs, and lists flows and runs. Only the two trigger listings
242
+ // refuse, and they refuse with the same typed issue `layerNone` answers,
243
+ // so a caller cannot tell an omitted port from a declared empty one.
244
+ const dispatch = Option.getOrElse(yield* Effect.serviceOption(DispatchReader.DispatchReader), DispatchReader.makeNone);
245
+ /**
246
+ * Whether this composition has an executor that can be asked about a run.
247
+ *
248
+ * Read once: the executor is captured when the layer is built, so the
249
+ * answer cannot change between two listings of one page.
250
+ */
251
+ const observing = Option.isSome(executor) && executor.value.readExecution !== undefined;
252
+ const observe = (run) => Effect.gen(function* () {
253
+ if (Option.isNone(executor) || executor.value.readExecution === undefined)
254
+ return run;
255
+ const observed = yield* executor.value.readExecution(run.runId);
256
+ if (observed._tag === "Missing")
257
+ return { ...run, executionObservation: "missing" };
258
+ return {
259
+ ...run,
260
+ executionObservation: "observed",
261
+ executionView: observed.executionView,
262
+ status: observed.status,
263
+ waitingReason: observed.waitingReason,
264
+ parentRunId: observed.parentRunId,
265
+ lineageId: observed.lineageId,
266
+ roundOrdinal: observed.roundOrdinal,
267
+ // The executor's answer replaces this plane's copy here as it does
268
+ // for every other observed field. It has to: a control plane over
269
+ // its own coordination database cannot see the executions a flow
270
+ // spawned, so a nested `HumanTask` park is a fact only the executor
271
+ // holds. Where the two share one database the runtime computes the
272
+ // same rows itself and the observation reproduces them.
273
+ pendingWaits: observed.pendingWaits
274
+ };
275
+ });
276
+ const getRun = (runId) => runtime.getRun(runId).pipe(Effect.flatMap(observe));
277
+ /** Fails `CodeDrift` when re-driving the run would enter code it did not start on. */
278
+ const refuseCodeDrift = (runId) => runtime.codeDrift(runId).pipe(Effect.flatMap((drift) => drift === undefined ? Effect.void : Effect.fail(drift)));
279
+ /**
280
+ * The run with the drift a resume would refuse, for an operator reading it.
281
+ * A drift that cannot be computed is left absent rather than failing the read.
282
+ */
283
+ const withCodeDrift = (run) => terminal(run.status)
284
+ ? Effect.succeed(run)
285
+ : runtime.codeDrift(run.runId).pipe(Effect.map((drift) => drift === undefined ? run : {
286
+ ...run,
287
+ codeDrift: {
288
+ ...(drift.recorded === undefined ? {} : { recorded: drift.recorded }),
289
+ ...(drift.current === undefined ? {} : { current: drift.current }),
290
+ ...(drift.recordedEngine === undefined ? {} : { recordedEngine: drift.recordedEngine }),
291
+ ...(drift.currentEngine === undefined ? {} : { currentEngine: drift.currentEngine })
292
+ }
293
+ }), Effect.orElseSucceed(() => run));
294
+ const mutationSemaphore = yield* Semaphore.make(1);
295
+ /** Journals one control event and answers its journal sequence. */
296
+ const record = (runId, eventType, payload) =>
297
+ // Unfenced: the control plane mutates runs it does not own — that is
298
+ // the point of a control plane — so its event records are
299
+ // first-writer-wins admissions, not owner-fenced lifecycle writes.
300
+ journal.emitDurableUnfenced(new JournalEvent.Input({
301
+ runId: JournalEvent.RunId.make(runId),
302
+ sourceId,
303
+ eventType,
304
+ payload: json(payload)
305
+ })).pipe(Effect.map((receipt) => receipt.seq), Effect.mapError((cause) => new PersistenceError({
306
+ operation: eventType,
307
+ message: `Failed to persist ${eventType}`,
308
+ cause
309
+ })));
310
+ const emit = (runId, eventType, payload) => Effect.asVoid(record(runId, eventType, payload));
311
+ /**
312
+ * Ends a run the executor was handed and could not take.
313
+ *
314
+ * `ControlExecutor.launch` fails when nothing in this composition will
315
+ * ever drive the run: no seat resolved, the flow declares none, the body
316
+ * would not load, the provider could not be constructed. The run row
317
+ * and its idempotency receipt are already durable by then. Keep that
318
+ * admission and record the failure; replaying its key must never launch
319
+ * another run. An explicit retry uses a new key.
320
+ *
321
+ * A settlement that cannot be written is logged rather than raised. The
322
+ * caller is already receiving the refusal it has to act on, and replacing
323
+ * it with a persistence error would hide which key is missing.
324
+ */
325
+ const settleUnlaunched = (runId, cause, fault) => Effect.gen(function* () {
326
+ const fence = yield* runtime.claimFence(runId);
327
+ const run = yield* runtime.writeStatus(runId, fence, "failed");
328
+ yield* emit(runId, "control.run.failed", json({ runId, status: "failed", cause: cause.slice(0, 4096), fault, ...ControlFacts.runFact(run) }));
329
+ }).pipe(journal.transact, Effect.catchCause((failure) => Effect.annotateLogs(Effect.logWarning("A refused launch could not be settled"), { runId, cause: Cause.pretty(failure) })));
330
+ const transact = (operation, effect) => mutationSemaphore.withPermits(1)(journal.transact(effect).pipe(Effect.mapError((cause) => cause instanceof Journal.JournalError
331
+ ? new PersistenceError({
332
+ operation: `${operation}.idempotency`,
333
+ message: `Failed to commit ${operation} and its idempotency receipt atomically`,
334
+ cause
335
+ })
336
+ : cause)));
337
+ /**
338
+ * Runs one mutation under its idempotency key.
339
+ *
340
+ * `replay` is what a recorded receipt is worth on a second ask. For every
341
+ * mutation that CHANGES something — a launch, a decision, a signal — it is
342
+ * everything: the receipt is the proof the change was made once, and
343
+ * replaying it is the whole guarantee.
344
+ *
345
+ * `cancel` is the exception, and it is `replay: false`. Its receipt is an
346
+ * answer ABOUT a run, and the run can be in a different state by the time
347
+ * the operator asks again: a cancel against a run a live peer owns answers
348
+ * `Accepted` and finishes nothing, and replaying that answer as
349
+ * `AlreadyApplied` turned a run nobody could reach into a run nobody could
350
+ * ask about either — the release validation left two of them, with `smithers
351
+ * cancel` and `smithers down` both answering from the receipt and neither
352
+ * ever reaching the row. Cancellation needs no receipt to be idempotent:
353
+ * the run's own terminality is stronger, and `cancel` reads it first and
354
+ * answers `Terminal` without touching anything.
355
+ */
356
+ const mutate = (operation, key, principal, mutationFingerprint, effect, replay = true, claimRunKey = false) => transact(operation, Effect.gen(function* () {
357
+ const durableKey = mutationKey(operation, key, principal);
358
+ const prior = yield* runtime.lookupMutation(durableKey, mutationFingerprint);
359
+ if (prior !== undefined && (replay || prior._tag === "Conflict")) {
360
+ return prior._tag === "AlreadyApplied"
361
+ ? { ...prior, receiptId: key }
362
+ : prior;
363
+ }
364
+ const claim = claimRunKey
365
+ ? yield* runtime.claimRunKey(durableKey, mutationFingerprint)
366
+ : undefined;
367
+ if (claim?._tag === "Raced")
368
+ return alreadyApplied(key, claim.receipt);
369
+ return yield* Effect.gen(function* () {
370
+ const receipt = yield* effect;
371
+ // A key that already carries a receipt is not re-recorded: the store
372
+ // refuses to overwrite one, and the answer this call returns is the
373
+ // fresh read of the run rather than the record.
374
+ if (receipt._tag === "Parked" && claimRunKey) {
375
+ yield* runtime.releaseRunKey(durableKey);
376
+ }
377
+ else if (receipt._tag !== "Parked" && prior === undefined) {
378
+ yield* runtime.recordMutation(durableKey, mutationFingerprint, receipt);
379
+ }
380
+ return receipt;
381
+ }).pipe(Effect.onExit((exit) => claim?._tag === "Claimed" && Exit.isFailure(exit)
382
+ ? runtime.releaseRunKey(durableKey)
383
+ : Effect.void));
384
+ }));
385
+ /**
386
+ * Hands a decided run's resume to whoever hosts its execution.
387
+ *
388
+ * Outside the mutation's write transaction, for `signal`'s reason: taking
389
+ * the resume up re-drives the run, and the engine's own writes would wait
390
+ * on the writer the transaction holds. A host that answers `resuming` has
391
+ * claimed the row and is driving, so its delegation is cleared here; every
392
+ * other composition leaves it standing for the host's own poll.
393
+ */
394
+ const takeUpResume = (runId, sequence) => Option.isNone(executor)
395
+ ? Effect.void
396
+ : executor.value.resumeRun({ runId }).pipe(Effect.flatMap((uptake) => uptake === "resuming" ? runtime.clearResume(runId, sequence) : Effect.void));
397
+ /**
398
+ * Hands an admitted run to the executor, then records its acknowledgment.
399
+ *
400
+ * Outside the admission's transaction: the executor may immediately read
401
+ * through another connection or fork a driver, so the run, its approval
402
+ * and its receipt are committed before any execution crosses this line.
403
+ */
404
+ const hand = (launch) => Effect.gen(function* () {
405
+ const acceptance = Option.isSome(executor)
406
+ ? yield* executor.value.launch(launch).pipe(Effect.tapError((error) => settleUnlaunched(error.runId, error.message, Fault.of(error))))
407
+ : "pending";
408
+ yield* transact("run.acceptance", Effect.gen(function* () {
409
+ // A fast executor may already have completed or parked. Never
410
+ // regress its durable outcome with the launch acknowledgment.
411
+ const current = yield* runtime.getRun(launch.run.runId);
412
+ if (current.status !== "accepted")
413
+ return;
414
+ const fence = yield* runtime.claimFence(current.runId);
415
+ const run = acceptance === "accepted"
416
+ ? yield* runtime.writeStatus(current.runId, fence, "running")
417
+ : yield* runtime.releasePending(current.runId, fence);
418
+ yield* emit(run.runId, acceptance === "accepted" ? "control.run.running" : "control.run.pending", {
419
+ runId: run.runId,
420
+ status: run.status,
421
+ ...ControlFacts.runFact(run)
422
+ });
423
+ }));
424
+ });
425
+ /**
426
+ * Launches a run whose admitting process died before handing it over.
427
+ *
428
+ * The admission and its receipt commit before {@link hand}, so a crash in
429
+ * between left the run `accepted` under its dead admitter, and a retry of
430
+ * the same key only read the receipt back: nothing ever launched it. The
431
+ * acknowledgment is what `accepted` with an owner lacks — a queued launch
432
+ * releases its owner. This process owning the run means another call here
433
+ * is between its admission and its launch; a live peer owning it is the
434
+ * same case elsewhere and keeps it (`ClaimLost`). Only a dead owner's run is
435
+ * claimed, under the run store's fence, so two retries launch it once.
436
+ */
437
+ const relaunchStranded = (runId) => Effect.gen(function* () {
438
+ const current = yield* runtime.getRun(runId);
439
+ if (current.status !== "accepted" || current.ownerId === undefined)
440
+ return;
441
+ const ours = yield* runtime.claimFence(runId).pipe(Effect.as(true), Effect.catchTag("/control/ClaimLost", () => Effect.succeed(false)));
442
+ if (ours)
443
+ return;
444
+ yield* refuseCodeDrift(runId);
445
+ const claimed = yield* runtime.resume(runId, { scope: "launched" }).pipe(Effect.catchTag("/control/ClaimLost", () => Effect.succeed(undefined)));
446
+ if (claimed === undefined || claimed.status !== "accepted" || claimed.planId === undefined)
447
+ return;
448
+ yield* hand({ plan: yield* runtime.getPlan(claimed.planId), run: claimed });
449
+ }).pipe(
450
+ // The recorded receipt is the retry's answer either way; a relaunch
451
+ // that fails leaves the run where it was for the next retry.
452
+ Effect.catch((failure) => Effect.annotateLogs(Effect.logWarning("A stranded run could not be relaunched"), { runId, cause: failure.message })));
453
+ const decide = (decision, submitted) => Effect.gen(function* () {
454
+ const input = yield* snapshotApproval(decision, submitted);
455
+ // Check the authenticated identity before replay or any grant writes.
456
+ const principal = yield* runtime.stampPrincipal(input.principal);
457
+ return yield* decideAs(decision, input, principal).pipe(
458
+ // Every refusal leaves a durable audit record: who asked, for which
459
+ // target, under which scope. The journal row's time is when. It is
460
+ // written after the refused mutation rolled back, so it is the only
461
+ // row the refusal adds; a record that cannot be written fails the
462
+ // call rather than leaving an unaudited refusal.
463
+ Effect.catchTag("/control/Unauthorized", (refusal) => emit(input.target._tag === "Plan" ? `plan:${input.target.planId}` : input.target.runId, "control.approval.refused", json({ decision, principal, scope: input.scope, target: input.target })).pipe(Effect.andThen(Effect.fail(refusal)))));
464
+ });
465
+ const decideAs = (decision, input, principal) => Effect.gen(function* () {
466
+ // Authorization precedes target reads and idempotency replay: neither
467
+ // an old receipt nor a terminal run confers authority on this caller.
468
+ yield* runtime.authorizeApproval({ principal, target: input.target, decision, scope: input.scope });
469
+ // A decision on a settled run decides nothing, and it is read BEFORE
470
+ // the idempotency replay for `resume`'s reason: the recorded receipt
471
+ // describes the earlier call, not the run. Answering `Accepted` sent
472
+ // `smithers approve` into `awaitRun` waiting for a settlement that had
473
+ // already happened — the release validation's 120-second silent block — and
474
+ // recorded a resume delegation for a run no host may take up.
475
+ //
476
+ // A plan-level decision has no run yet, and a target whose run this
477
+ // plane cannot find is left to `lookupApproval` to refuse.
478
+ if (input.target._tag === "Node") {
479
+ const current = yield* getRun(input.target.runId).pipe(Effect.catchTag("/control/RunNotFound", () => Effect.succeed(undefined)));
480
+ if (current !== undefined && terminal(current.status)) {
481
+ const settled = { _tag: "Terminal", runId: current.runId, status: current.status };
482
+ return settled;
483
+ }
484
+ }
485
+ // Set by the mutation when it records a delegation, and left unset on
486
+ // the idempotency replay path — where the original call already
487
+ // delegated and the host's own poll is what takes it up.
488
+ let delegated;
489
+ const receipt = yield* mutate(decision, input.idempotencyKey, principal, fingerprint(decision, principal, input), Effect.gen(function* () {
490
+ // A node decision restarts the run, so it re-enters the flow's
491
+ // current code: a changed flow is refused before anything is
492
+ // resolved, and the run stays waiting for this decision.
493
+ if (input.target._tag === "Node")
494
+ yield* refuseCodeDrift(input.target.runId);
495
+ const token = yield* runtime.lookupApproval(input.target);
496
+ // Resolve (and recheck authority) before installing any grant. The
497
+ // durable adapter commits all three writes in this transaction;
498
+ // the memory test adapter must also leave no grant on refusal.
499
+ yield* runtime.resolveApproval(token, decision, principal, input.scope);
500
+ if (decision === "approved") {
501
+ yield* runtime.installBulkGrant(token, input.target.envelope, input.scope);
502
+ }
503
+ yield* emit(input.target._tag === "Plan" ? `plan:${input.target.planId}` : input.target.runId, `control.approval.${decision}`, json({
504
+ ...ControlFacts.approvalDecisionFact(token.tokenId, input.target),
505
+ target: input.target._tag,
506
+ scope: input.scope,
507
+ envelope: input.target.envelope,
508
+ principal
509
+ }));
510
+ if (input.target._tag === "Plan")
511
+ return accepted(input.idempotencyKey);
512
+ // A decision on a node target has to restart the run the ask parked,
513
+ // in this call. Answering without a restart left the run in
514
+ // `waiting-approval` until a second call arrived, and a denial the
515
+ // run never learns about is a denial that decided nothing.
516
+ //
517
+ // The restart is recorded, not performed, and this plane does NOT
518
+ // claim the row. `scope: "launched"` reads like a process scope and
519
+ // is not one — it is a `control_runs` lookup, a durable table every
520
+ // process over one control database shares — so claiming here took
521
+ // the row away from the host that could still drive it, and left it
522
+ // `accepted` under a process with no executor. The delegation is
523
+ // durable instead: the host takes it up on its next poll and clears
524
+ // it, and the journal entry stays as the operator's record of why
525
+ // (triage B-15).
526
+ const runId = input.target.runId;
527
+ delegated = yield* runtime.requestResume(runId);
528
+ yield* emit(runId, "control.run.resumed", { runId });
529
+ return accepted(input.idempotencyKey, runId);
530
+ }));
531
+ if (input.target._tag === "Node" && delegated !== undefined) {
532
+ yield* takeUpResume(input.target.runId, delegated);
533
+ }
534
+ return receipt;
535
+ });
536
+ /**
537
+ * Restarts a parked run, by claiming it or by asking whoever owns it.
538
+ *
539
+ * A run this plane launched is this plane's to claim, and `scope:
540
+ * "launched"` is how the runtime is told to check. A run the ENGINE created
541
+ * — a child, a fork, a trampoline round — has its own driver, and claiming
542
+ * it here overwrote the engine's `state_json` and owner columns with a
543
+ * control-plane summary, after which that driver's `scheduleResume` no
544
+ * longer recognized the row: the run stayed suspended with its waiting
545
+ * reason set and its execution never returned (control-plane example 38).
546
+ *
547
+ * Both public resume spellings journal `control.run.resume`. The caller
548
+ * or a host-supplied journal subscriber must drive the execution, and
549
+ * this path never offers `executor.resumeRun`. A run a live peer is
550
+ * HOLDING — `running`, or the `accepted` a claim writes and only a
551
+ * settlement rewrites — is still `ClaimLost`: there is nothing to
552
+ * restart, and pretending otherwise would hide the peer.
553
+ *
554
+ * A run a live host PARKED is that host's to drive: claiming it would
555
+ * steal its execution (#3342), and refusing left a lease-lapsed park
556
+ * nothing could restart. The resume is handed to the host instead. The
557
+ * journal entry names the host, and a durable delegation carries its
558
+ * sequence as the operator's consent, which the host records as the
559
+ * per-release retry permission before it re-drives the run (#2982). The
560
+ * receipt's `handedTo` tells the caller it holds nothing to drive.
561
+ */
562
+ const runMutation = (submitted) => Effect.gen(function* () {
563
+ const input = yield* snapshotResume(submitted);
564
+ // Terminality is read BEFORE the idempotency replay, as `cancel` reads
565
+ // it. A recorded receipt is the proof a restart was made once; it is
566
+ // not an answer about the run, and the run settles afterwards. The
567
+ // release validation asked `run --resume` for a completed run and was told
568
+ // `AlreadyApplied`, which describes the earlier call and says nothing
569
+ // about the run the operator named (spec item 3).
570
+ const settled = yield* getRun(input.runId);
571
+ if (terminal(settled.status)) {
572
+ return { _tag: "Terminal", runId: settled.runId, status: settled.status };
573
+ }
574
+ const principal = yield* runtime.stampPrincipal(input.principal);
575
+ return yield* mutate("resume", input.idempotencyKey, principal, fingerprint("resume", principal, input), Effect.gen(function* () {
576
+ const current = yield* getRun(input.runId);
577
+ if (terminal(current.status)) {
578
+ return { _tag: "Terminal", runId: current.runId, status: current.status };
579
+ }
580
+ // Every claim re-enters the flow's current code, so a changed flow
581
+ // is refused before the claim and the run stays where it was.
582
+ if (input.allowCodeDrift !== true)
583
+ yield* refuseCodeDrift(input.runId);
584
+ let handedTo;
585
+ const claimed = yield* (input.allowCodeDrift === true
586
+ ? runtime.resumeAdopting(input.runId, { scope: "launched" })
587
+ : runtime.resume(input.runId, { scope: "launched" })).pipe(Effect.catchTag("/control/ClaimLost", (error) => {
588
+ if (error.parkedBy !== undefined) {
589
+ handedTo = error.parkedBy;
590
+ return Effect.succeed(undefined);
591
+ }
592
+ if (error.reason !== undefined)
593
+ return Effect.fail(error);
594
+ return runtime.getRun(input.runId).pipe(Effect.flatMap((stored) =>
595
+ // A retained fork is parked in control and pending in the
596
+ // engine. Its accepted read overlay is not a live claim.
597
+ // Never replace that engine continuation with control state.
598
+ live(stored.status) ||
599
+ (live(current.status) && current.executionView?.current.status !== "pending")
600
+ ? Effect.fail(new ClaimLost({ runId: input.runId }))
601
+ : Effect.succeed(undefined)));
602
+ }));
603
+ // The same attribution `cancel` writes, for the same reason: the
604
+ // contract records `reason` on the journal entry the mutation
605
+ // writes and `principal` as stamped by the runtime, and a resume
606
+ // that carried neither left an operator unable to say who restarted
607
+ // a run or why.
608
+ const sequence = yield* record(input.runId, "control.run.resume", json({
609
+ runId: input.runId,
610
+ status: (claimed ?? current).status,
611
+ ...(claimed === undefined ? {} : ControlFacts.runFact(claimed)),
612
+ principal,
613
+ ...(input.reason === undefined ? {} : { reason: input.reason }),
614
+ ...(handedTo === undefined ? {} : { handedTo })
615
+ }));
616
+ if (handedTo !== undefined) {
617
+ yield* runtime.requestResume(input.runId, { consent: sequence });
618
+ const handedOff = {
619
+ _tag: "Accepted",
620
+ receiptId: input.idempotencyKey,
621
+ runId: input.runId,
622
+ handedTo
623
+ };
624
+ return handedOff;
625
+ }
626
+ return claimed === undefined
627
+ ? accepted(input.idempotencyKey, input.runId)
628
+ : terminalOrAccepted(input.idempotencyKey, claimed);
629
+ }));
630
+ });
631
+ /**
632
+ * Makes a cancellation durable on the engine row through the executor.
633
+ *
634
+ * Absent executor, absent engine: the composition runs nothing, so there is
635
+ * no row to write and the local interrupt is the whole cancel. An executor
636
+ * that answers `unknown` has an engine that never heard of the run, which
637
+ * is the same situation with a different messenger.
638
+ */
639
+ const executorRequestCancel = (runId) => Option.isNone(executor)
640
+ ? Effect.succeed("unknown")
641
+ : executor.value.requestCancel({ runId });
642
+ /**
643
+ * Finishes the parked execution the cancel just recorded a request on.
644
+ *
645
+ * Outside the mutation's write transaction, for `takeUpResume`'s reason:
646
+ * settling a park re-enters the engine, and the engine's writes would wait
647
+ * on the writer the transaction holds — which deadlocks the cancel rather
648
+ * than slowing it. So this runs on the way out, once the request and the
649
+ * terminal control status are both committed.
650
+ */
651
+ const executorSettleCancelledPark = (runId) => Option.isNone(executor)
652
+ ? Effect.void
653
+ : executor.value.settleCancelledPark({ runId });
654
+ /**
655
+ * Moves this plane's row onto the status the engine already reached.
656
+ *
657
+ * A reconciliation that cannot be written is logged rather than raised, for
658
+ * `settleUnlaunched`'s reason: the caller is already receiving the engine's
659
+ * terminal receipt, which is the true answer, and a live peer holding the
660
+ * row will settle it itself.
661
+ */
662
+ const reconcileTerminal = (runId, status) => Effect.gen(function* () {
663
+ // Reconcile the coordination row itself. Applying the engine overlay
664
+ // here would make a stale row appear settled before it was persisted.
665
+ const current = yield* runtime.getRun(runId);
666
+ if (terminal(current.status))
667
+ return;
668
+ const fence = yield* runtime.claimFence(runId).pipe(
669
+ // A parked coordination row has released its fence. Claim that row
670
+ // before copying the engine's terminal fact; a live peer still wins.
671
+ Effect.catchTag("/control/ClaimLost", () => runtime.resume(runId).pipe(Effect.andThen(runtime.claimFence(runId)))));
672
+ const run = yield* runtime.writeStatus(runId, fence, status);
673
+ yield* emit(runId, `control.run.${status}`, json({ runId, status, ...ControlFacts.runFact(run) }));
674
+ }).pipe(journal.transact, Effect.catchCause((failure) => Effect.annotateLogs(Effect.logWarning("A settled engine row could not be reconciled onto the control row"), { runId, status, cause: Cause.pretty(failure) })));
675
+ /**
676
+ * Resumes a parked run whose park a steer has just answered.
677
+ *
678
+ * Only two parks are the steer's to end. A run parked on `event` is
679
+ * waiting for something to arrive, and a steer is something arriving. A
680
+ * run parked on `released` lost its owner to a sweep
681
+ * (`@smthrs/engine-store` `DisasterRecovery.fence`) and nothing is coming
682
+ * to claim it, so the steer claims it.
683
+ *
684
+ * Every other park keeps waiting. An `approval`, `timer`, or `quota` park
685
+ * is waiting for a decision, a clock, or a budget that a message does not
686
+ * supply. A park with NO reason at all is an operator's own park, written
687
+ * through `ControlRuntime.writeStatus`, and it is the one park a steer must
688
+ * not end: an operator who stopped a run and then sent it a message is
689
+ * queuing the message for when they restart it, not asking for the stop to
690
+ * be undone. A park a control plane cannot explain is left alone for the
691
+ * same reason.
692
+ *
693
+ * A lost claim is not a failure here. It means another process already
694
+ * owns the run, or the run belongs to a driver this plane did not launch
695
+ * — an engine-created child keeps its park, because claiming it would
696
+ * strand it under this plane's fence where no engine re-drives it. The
697
+ * steer itself is already durable in the notification queue, so the
698
+ * owning driver delivers it at the run's next boundary. A refusal naming
699
+ * the live parked host delegates a wake to that host, so an idle retained
700
+ * module gets that boundary without transferring its fence.
701
+ */
702
+ const wake = (run, messageId) => {
703
+ if (run.status !== "parked")
704
+ return Effect.void;
705
+ if (run.waitingReason !== "event" && run.waitingReason !== "released")
706
+ return Effect.void;
707
+ // A changed flow keeps its park: the steer stays queued, and the
708
+ // operator decides with `runs resume --allow-code-drift`.
709
+ return refuseCodeDrift(run.runId).pipe(Effect.andThen(runtime.resume(run.runId, { scope: "launched" })), Effect.flatMap((resumed) => emit(run.runId, "control.steer.woke", {
710
+ runId: run.runId,
711
+ messageId,
712
+ status: resumed.status,
713
+ ...ControlFacts.runFact(resumed)
714
+ })),
715
+ // A detached live host keeps its fence. Delegate the admitted event
716
+ // wake to it, just as a node decision does, instead of leaving an idle
717
+ // retained module with input it will never get a boundary to drain.
718
+ Effect.catchTag("/control/ClaimLost", (refusal) => refusal.parkedBy === undefined ? Effect.void : Effect.gen(function* () {
719
+ const delegated = yield* runtime.requestResume(run.runId).pipe(Effect.catchTag("/control/InvalidInput", () => Effect.succeed(undefined)));
720
+ if (delegated !== undefined)
721
+ yield* emit(run.runId, "control.run.resumed", { runId: run.runId });
722
+ })), Effect.catchTag("/control/RunNotFound", () => Effect.void), Effect.catchTag("/control/CodeDrift", (drift) => Effect.annotateLogs(Effect.logWarning("A steer did not wake a run whose code changed"), {
723
+ runId: run.runId,
724
+ cause: drift.message
725
+ })));
726
+ };
727
+ /**
728
+ * A page of run summaries with their pending steer counts filled in.
729
+ *
730
+ * The count comes from the notification queue rather than from a column,
731
+ * because pending is admitted minus promoted and the queue owns both
732
+ * halves. A queue that is unavailable leaves the field absent — "not
733
+ * known" is representable, and it is the truth — while a journal that
734
+ * fails is a failed listing.
735
+ */
736
+ const withSteering = (runs) => Effect.forEach(runs, (run) => notifications.pending(run.runId).pipe(Effect.map((pending) => ({
737
+ ...run,
738
+ steering: {
739
+ pending: pending.filter((notification) => notification.delivery === "steer").length
740
+ }
741
+ })), Effect.catchTag("/notifications/NotificationError", () => Effect.succeed(run)), Effect.mapError((cause) => new PersistenceError({
742
+ operation: "control.list.steering",
743
+ message: `Failed to read pending steering for ${run.runId}`,
744
+ cause
745
+ }))));
746
+ /**
747
+ * The runs every recorded fire of a trigger started. A reader answers the
748
+ * ledger prefix a page at `cursor` needs, so the window widens until the
749
+ * reader answers less than it could.
750
+ */
751
+ const triggerRuns = (triggerId) => Effect.gen(function* () {
752
+ let start = 0;
753
+ while (true) {
754
+ const fires = yield* dispatch.fires({
755
+ _tag: "fires",
756
+ filters: { triggerId },
757
+ limit: maxPageSize,
758
+ ...(start === 0 ? {} : { cursor: String(start) })
759
+ });
760
+ if (fires.length <= start + maxPageSize) {
761
+ return fires.flatMap((fire) => fire.triggerId === triggerId && fire.runId !== undefined ? [fire.runId] : []);
762
+ }
763
+ start = Math.max(fires.length, start * 2);
764
+ }
765
+ });
766
+ /**
767
+ * Whether a restricted reader may see `runId`: only a run this plane
768
+ * launched for the reader's own principal. A plan partition, an
769
+ * engine-created run, and a run that does not exist are all invisible.
770
+ */
771
+ const readerSees = (reader, runId) => runId.startsWith("plan:")
772
+ ? Effect.succeed(false)
773
+ : runtime.getRun(runId).pipe(Effect.map((run) => launchedByMatches(run, reader)), Effect.catchTag("/control/RunNotFound", () => Effect.succeed(false)));
774
+ /**
775
+ * Confines a mutation to a run its reader may see, with the rule `List`
776
+ * and `Watch` use: another principal's run answers `RunNotFound` exactly
777
+ * as a missing one, before any idempotency replay can describe it.
778
+ */
779
+ const confined = (submitted, mutation) => submitted.reader === undefined ? mutation : Effect.flatMap(readerSees(submitted.reader, submitted.runId), (visible) => visible ? mutation : Effect.fail(new RunNotFound({ runId: submitted.runId })));
780
+ const list = (request) => Effect.gen(function* () {
781
+ if (request._tag === "executions") {
782
+ if (request.reader !== undefined && !(yield* readerSees(request.reader, request.runId))) {
783
+ return yield* new RunNotFound({ runId: request.runId });
784
+ }
785
+ yield* runtime.getRun(request.runId);
786
+ yield* Schema.decodeUnknownEffect(ListRequest)(request).pipe(Effect.mapError(() => invalid("executionIds: at most 200 nonempty execution IDs are admitted")));
787
+ const batch = Option.isSome(executor) && executor.value.readExecutions !== undefined
788
+ ? yield* executor.value.readExecutions(request)
789
+ : {
790
+ source: null,
791
+ revision: null,
792
+ snapshots: request.executionIds.map((executionId) => ({
793
+ _tag: "Unavailable",
794
+ executionId,
795
+ reason: "unsupported"
796
+ }))
797
+ };
798
+ return { _tag: "executions", source: batch.source, revision: batch.revision, items: batch.snapshots };
799
+ }
800
+ const bounds = yield* pageBounds(request._tag === "runs" ? undefined : request.cursor, request.limit);
801
+ if (request._tag === "plans") {
802
+ // A plan's input and envelope are an operator's to read.
803
+ if (request.reader !== undefined)
804
+ return { _tag: "plans", items: [] };
805
+ const result = yield* runtime.queryPlans({
806
+ flowId: request.filters?.flowId,
807
+ decision: request.filters?.decision,
808
+ after: bounds.start,
809
+ limit: bounds.size
810
+ });
811
+ const items = result.plans.map((plan) => ({
812
+ card: plan.card,
813
+ // The JSON form the durable runtime stores, whichever runtime answers.
814
+ input: JSON.parse(JSON.stringify(plan.decodedInput ?? null)),
815
+ decision: plan.decision
816
+ }));
817
+ return result.next === undefined
818
+ ? { _tag: "plans", items }
819
+ : { _tag: "plans", items, nextCursor: String(result.next) };
820
+ }
821
+ if (request._tag === "flows") {
822
+ const [registered, warnings] = yield* Effect.all([registry.list(), registry.warnings()]);
823
+ const available = registered.length > 0
824
+ ? registered.map((descriptor) => ({
825
+ flowId: descriptor.name,
826
+ description: descriptor.description,
827
+ ...(inputDocument(descriptor.input) === undefined
828
+ ? {}
829
+ : { inputSchema: inputDocument(descriptor.input) })
830
+ }))
831
+ : yield* runtime.listFlows;
832
+ const result = page(available, bounds);
833
+ const diagnostics = warnings.length === 0 ? {} : { warnings };
834
+ return result.nextCursor === undefined
835
+ ? { _tag: "flows", items: result.items, ...diagnostics }
836
+ : { _tag: "flows", items: result.items, ...diagnostics, nextCursor: result.nextCursor };
837
+ }
838
+ if (request._tag === "triggers") {
839
+ // A trigger's input and active run belong to whoever configured it;
840
+ // like a plan, it is an operator's to read.
841
+ if (request.reader !== undefined)
842
+ return { _tag: "triggers", items: [] };
843
+ let triggers = yield* dispatch.list(request);
844
+ if (request.filters?.triggerId !== undefined) {
845
+ triggers = triggers.filter((trigger) => trigger.triggerId === request.filters?.triggerId);
846
+ }
847
+ if (request.filters?.flowId !== undefined) {
848
+ triggers = triggers.filter((trigger) => trigger.flowId === request.filters?.flowId);
849
+ }
850
+ if (request.filters?.enabled !== undefined) {
851
+ triggers = triggers.filter((trigger) => trigger.enabled === request.filters?.enabled);
852
+ }
853
+ const result = page(triggers, bounds);
854
+ return result.nextCursor === undefined
855
+ ? { _tag: "triggers", items: result.items }
856
+ : { _tag: "triggers", items: result.items, nextCursor: result.nextCursor };
857
+ }
858
+ if (request._tag === "fires") {
859
+ let fires = yield* dispatch.fires(request);
860
+ const reader = request.reader;
861
+ if (reader !== undefined) {
862
+ // A restricted reader sees the fires that started its own runs.
863
+ const seen = yield* Effect.forEach(fires, (fire) => fire.runId === undefined ? Effect.succeed(false) : readerSees(reader, fire.runId));
864
+ fires = fires.filter((_, index) => seen[index]);
865
+ }
866
+ if (request.filters?.triggerId !== undefined) {
867
+ fires = fires.filter((fire) => fire.triggerId === request.filters?.triggerId);
868
+ }
869
+ if (request.filters?.runId !== undefined) {
870
+ fires = fires.filter((fire) => fire.runId === request.filters?.runId);
871
+ }
872
+ if (request.filters?.outcome !== undefined) {
873
+ fires = fires.filter((fire) => fire.outcome === request.filters?.outcome);
874
+ }
875
+ const result = page(fires, bounds);
876
+ return result.nextCursor === undefined
877
+ ? { _tag: "fires", items: result.items }
878
+ : { _tag: "fires", items: result.items, nextCursor: result.nextCursor };
879
+ }
880
+ const filters = request.filters;
881
+ // A restricted reader lists only its own runs, whatever it asked for:
882
+ // naming another principal's id selects nothing.
883
+ const reader = request.reader;
884
+ if (reader !== undefined && filters?.principalId !== undefined && filters.principalId !== reader.id) {
885
+ return { _tag: "runs", items: [] };
886
+ }
887
+ const launcher = reader !== undefined
888
+ ? { id: reader.id, kind: reader.kind }
889
+ : filters?.principalId === undefined
890
+ ? undefined
891
+ : { id: filters.principalId };
892
+ const fingerprint = JSON.stringify([
893
+ filters?.runId ?? null,
894
+ filters?.flowId ?? null,
895
+ filters?.status ?? null,
896
+ filters?.parentRunId ?? null,
897
+ filters?.lineageId ?? null,
898
+ // Keep legacy cursor fingerprints byte-identical unless opting in.
899
+ ...(filters?.terminal === undefined && request.order === undefined
900
+ ? []
901
+ : [filters?.terminal ?? null, request.order ?? null]),
902
+ ...(filters?.since === undefined && filters?.until === undefined && filters?.triggerId === undefined
903
+ ? []
904
+ : [filters.since ?? null, filters.until ?? null, filters.triggerId ?? null]),
905
+ ...(launcher === undefined ? [] : [launcher.id, launcher.kind ?? null])
906
+ ]);
907
+ const cursor = request.cursor === undefined ? undefined : yield* Schema.decodeUnknownEffect(runCursor)(request.cursor).pipe(Effect.mapError(() => invalid("cursor: expected a run listing cursor")));
908
+ if (cursor !== undefined && cursor.filters !== fingerprint) {
909
+ return yield* invalid("cursor: belongs to different run filters");
910
+ }
911
+ // Exact lookups retain their one-row path. Other queries select durable
912
+ // summary fields in the adapter before observing the selected page.
913
+ if (filters?.runId !== undefined) {
914
+ let runs = yield* getRun(filters.runId).pipe(Effect.map((run) => [run]), Effect.catchTag("/control/RunNotFound", () => Effect.succeed([])));
915
+ if (filters.flowId !== undefined)
916
+ runs = runs.filter((run) => run.flowId === filters.flowId);
917
+ if (filters.status !== undefined)
918
+ runs = runs.filter((run) => run.status === filters.status);
919
+ if (filters.terminal !== undefined)
920
+ runs = runs.filter((run) => terminal(run.status) === filters.terminal);
921
+ if (filters.parentRunId !== undefined)
922
+ runs = runs.filter((run) => run.parentRunId === filters.parentRunId);
923
+ if (filters.lineageId !== undefined)
924
+ runs = runs.filter((run) => run.lineageId === filters.lineageId);
925
+ if (launcher !== undefined)
926
+ runs = runs.filter((run) => launchedByMatches(run, launcher));
927
+ if (filters.since !== undefined)
928
+ runs = runs.filter((run) => run.createdAt >= filters.since);
929
+ if (filters.until !== undefined)
930
+ runs = runs.filter((run) => run.createdAt < filters.until);
931
+ if (filters.triggerId !== undefined) {
932
+ const started = yield* triggerRuns(filters.triggerId);
933
+ runs = runs.filter((run) => started.includes(run.runId));
934
+ }
935
+ return { _tag: "runs", items: yield* withSteering(yield* Effect.forEach(runs, withCodeDrift)) };
936
+ }
937
+ // A status filter has to select on the status a caller will READ.
938
+ // `observe` replaces this plane's copy with the executor's, so a run
939
+ // the engine has parked on a human wait answers `waiting-approval`
940
+ // while the coordination row still says whatever it last recorded —
941
+ // and the adapter's own filter dropped it before anybody looked. That
942
+ // is what left workspace 6f2733a3's run-1 out of every
943
+ // `status: "waiting-approval"` page while the run tree was waiting on
944
+ // a person. The exact-lookup branch above has always filtered on the
945
+ // observed summary; this makes the paged branch agree.
946
+ //
947
+ // The source therefore selects without the status and the page is
948
+ // filled from observed rows, asking for only as many as are still
949
+ // needed so a page never over-delivers rows its cursor has passed. A
950
+ // filter the source cannot evaluate costs a walk, which is why the
951
+ // walk stops at a full page or at the end of the runs.
952
+ const postFiltered = observing && (filters?.status !== undefined || filters?.terminal !== undefined);
953
+ const { triggerId, principalId: _principalId, ...unowned } = filters ?? {};
954
+ const selected = launcher === undefined ? unowned : { ...unowned, launchedBy: launcher };
955
+ const narrowed = triggerId === undefined ? selected : { ...selected, runIds: yield* triggerRuns(triggerId) };
956
+ const sourceFilters = postFiltered
957
+ ? Object.fromEntries(Object.entries(narrowed).filter(([key]) => key !== "status" && key !== "terminal"))
958
+ : narrowed;
959
+ const collected = [];
960
+ let sourceCursor = cursor;
961
+ let sourceNext;
962
+ while (true) {
963
+ const result = yield* runtime.queryRuns({
964
+ filters: sourceFilters,
965
+ order: request.order,
966
+ cursor: sourceCursor,
967
+ limit: bounds.size - collected.length
968
+ });
969
+ const observed = yield* Effect.forEach(result.items, observe);
970
+ for (const run of observed) {
971
+ if (!postFiltered ||
972
+ (filters?.status === undefined || run.status === filters.status) &&
973
+ (filters?.terminal === undefined || terminal(run.status) === filters.terminal))
974
+ collected.push(run);
975
+ }
976
+ sourceNext = result.nextCursor;
977
+ if (!postFiltered || sourceNext === undefined || collected.length >= bounds.size)
978
+ break;
979
+ sourceCursor = { version: 1, filters: fingerprint, ...sourceNext };
980
+ }
981
+ const items = yield* withSteering(collected);
982
+ return sourceNext === undefined
983
+ ? { _tag: "runs", items }
984
+ : {
985
+ _tag: "runs",
986
+ items,
987
+ nextCursor: JSON.stringify({ version: 1, filters: fingerprint, ...sourceNext })
988
+ };
989
+ });
990
+ const streamForRun = (runId, filter) => journal.stream({
991
+ runId: JournalEvent.RunId.make(runId),
992
+ ...(filter.afterSequence === undefined
993
+ ? {}
994
+ : { afterSequence: JournalEvent.Seq.make(filter.afterSequence) })
995
+ }).pipe(Stream.map(eventFromEntry), Stream.mapError(watchReadFailed));
996
+ /**
997
+ * Finds the last committed sequence without walking the history. The
998
+ * journal's public cursor is forward-only, so exponential probes first
999
+ * bracket the tail and binary probes then pin it exactly. Only these
1000
+ * indexed one-row reads run in the transaction that fixes the cutoff.
1001
+ */
1002
+ const snapshotHighWater = (runId) => journal.transact(Effect.gen(function* () {
1003
+ const first = yield* journal.entries({ runId, limit: 1 });
1004
+ const initial = first.entries[0];
1005
+ if (initial === undefined)
1006
+ return undefined;
1007
+ let lower = initial.seq;
1008
+ let step = 1;
1009
+ let upper = lower;
1010
+ const maximumSequence = Number.MAX_SAFE_INTEGER - 1;
1011
+ while (lower < maximumSequence) {
1012
+ const probe = Math.min(maximumSequence, lower + step - 1);
1013
+ const next = yield* journal.entries({
1014
+ runId,
1015
+ after: JournalEvent.Seq.make(probe),
1016
+ limit: 1
1017
+ });
1018
+ const entry = next.entries[0];
1019
+ if (entry === undefined) {
1020
+ upper = probe;
1021
+ break;
1022
+ }
1023
+ lower = entry.seq;
1024
+ if (lower === maximumSequence)
1025
+ return entry.seq;
1026
+ step = Math.min(maximumSequence - lower, step * 2);
1027
+ }
1028
+ while (lower < upper) {
1029
+ const middle = lower + Math.ceil((upper - lower) / 2);
1030
+ const next = yield* journal.entries({
1031
+ runId,
1032
+ after: JournalEvent.Seq.make(middle - 1),
1033
+ limit: 1
1034
+ });
1035
+ const entry = next.entries[0];
1036
+ if (entry === undefined) {
1037
+ upper = middle - 1;
1038
+ }
1039
+ else {
1040
+ lower = entry.seq;
1041
+ }
1042
+ }
1043
+ return JournalEvent.Seq.make(lower);
1044
+ })).pipe(Effect.mapError(watchReadFailed));
1045
+ const snapshotForRunAt = (runId, filter, highWater) => {
1046
+ const journalRunId = JournalEvent.RunId.make(runId);
1047
+ const initialAfter = filter.afterSequence === undefined
1048
+ ? undefined
1049
+ : JournalEvent.Seq.make(filter.afterSequence);
1050
+ if (highWater === undefined || (initialAfter !== undefined && initialAfter >= highWater)) {
1051
+ return Stream.empty;
1052
+ }
1053
+ return Stream.paginate(initialAfter, (after) => journal.entries({
1054
+ runId: journalRunId,
1055
+ ...(after === undefined ? {} : { after }),
1056
+ limit: snapshotPageSize
1057
+ }).pipe(Effect.map((page) => {
1058
+ const entries = page.entries.filter((entry) => entry.seq <= highWater);
1059
+ const last = entries.at(-1);
1060
+ const next = last === undefined || last.seq >= highWater || !page.hasMore
1061
+ ? Option.none()
1062
+ : Option.some(last.seq);
1063
+ return [entries, next];
1064
+ }), Effect.mapError(watchReadFailed))).pipe(Stream.map(eventFromEntry));
1065
+ };
1066
+ const snapshotForRun = (runId, filter) => Stream.unwrap(Effect.map(snapshotHighWater(JournalEvent.RunId.make(runId)), (highWater) => snapshotForRunAt(runId, filter, highWater)));
1067
+ /**
1068
+ * Every journal partition, plans first, one inventory page at a time.
1069
+ *
1070
+ * The walk reads keys only and pulls the next page when the consumer asks
1071
+ * for it, so a global watch holds one page of ids rather than the whole
1072
+ * run table. Each inventory's first page pins its newest position and
1073
+ * every later page stops there, so the walk is finite however fast runs
1074
+ * are admitted: a partition that exists before the walk starts is listed
1075
+ * exactly once, and one created during the walk is left to the follow
1076
+ * tail.
1077
+ */
1078
+ const inventory = (page) => {
1079
+ const first = {};
1080
+ return Stream.paginate(first, (cursor) => Effect.map(page({ ...cursor, limit: partitionPageSize }), (next) => [
1081
+ next.ids,
1082
+ next.next === undefined ? Option.none() : Option.some({ after: next.next, through: next.through })
1083
+ ]));
1084
+ };
1085
+ const journalPartitions = Stream.concat(Stream.map(inventory(runtime.pagePlanIds), (planId) => `plan:${planId}`), inventory(runtime.pageRunIds));
1086
+ const snapshot = (filter) => filter.runId !== undefined
1087
+ ? snapshotForRun(filter.runId, filter)
1088
+ : Stream.flatMap(journalPartitions, (partition) => snapshotForRun(partition, filter), {
1089
+ concurrency: snapshotPartitionConcurrency
1090
+ });
1091
+ const entries = (filter) => filter.follow === false
1092
+ ? snapshot(filter)
1093
+ : filter.runId !== undefined
1094
+ ? streamForRun(filter.runId, filter)
1095
+ : Stream.unwrap(Effect.gen(function* () {
1096
+ const subscription = yield* journal.changes;
1097
+ // Subscribe first, then pin each partition's cutoff. A row
1098
+ // committed at or before its cutoff is read from the finite
1099
+ // snapshot; one committed after it is read from the buffered tail.
1100
+ // This is a handoff, not a bounded duplicate cache, so an
1101
+ // arbitrarily long history cannot make an old overlap reappear.
1102
+ //
1103
+ // A cutoff is pinned by whichever side reaches its partition
1104
+ // first: the paged walk, or the tail when an entry arrives for a
1105
+ // partition the walk has not reached or never lists (one created
1106
+ // after the walk passed its key). That side reads the partition's
1107
+ // snapshot; the other reuses the cutoff. The map keeps one cutoff
1108
+ // per partition seen for the life of the stream.
1109
+ const cutoffs = new Map();
1110
+ const pin = (partition) => {
1111
+ const pinned = Deferred.makeUnsafe();
1112
+ cutoffs.set(partition, pinned);
1113
+ return snapshotHighWater(JournalEvent.RunId.make(partition)).pipe(Effect.onExit((exit) => Deferred.done(pinned, exit)));
1114
+ };
1115
+ const walk = journalPartitions.pipe(Stream.mapEffect((partition) => Effect.suspend(() => cutoffs.has(partition)
1116
+ ? Effect.succeed(Option.none())
1117
+ : Effect.map(pin(partition), (highWater) => Option.some([partition, highWater]))), { concurrency: snapshotPartitionConcurrency }), Stream.filter(Option.isSome), Stream.map((pinned) => pinned.value), Stream.flatMap(([partition, highWater]) => snapshotForRunAt(partition, filter, highWater), {
1118
+ concurrency: snapshotPartitionConcurrency
1119
+ }));
1120
+ const cutoffFor = (partition) => Effect.suspend(() => {
1121
+ const known = cutoffs.get(partition);
1122
+ if (known !== undefined) {
1123
+ return Effect.map(Deferred.await(known), (highWater) => ({ highWater, claimed: false }));
1124
+ }
1125
+ return Effect.map(pin(partition), (highWater) => ({ highWater, claimed: true }));
1126
+ });
1127
+ /**
1128
+ * Detects a live tail that silently lost entries.
1129
+ *
1130
+ * `changes` is a sliding PubSub: a watcher that falls behind drops
1131
+ * committed entries with no signal, which turns this follow into a
1132
+ * permanently incomplete stream. Sequence numbers are
1133
+ * partition-local, so each tail entry is checked against the last
1134
+ * sequence this watcher saw for its partition (or the snapshot
1135
+ * cutoff for one it has not tailed yet).
1136
+ *
1137
+ * A gap is not proof of loss on its own: a rolled-back transaction
1138
+ * leaves an allocated sequence unused, and that hole is benign. The
1139
+ * durable journal disambiguates — an entry in the gap that was
1140
+ * committed but never delivered here is a real loss and fails the
1141
+ * stream, while a hole with no durable entry is skipped.
1142
+ *
1143
+ * One limitation, by design: the disambiguation reads the durable
1144
+ * journal, so an entry that was committed, dropped, and then
1145
+ * compacted away before the check runs reports as a benign hole.
1146
+ * Closing that needs a registered reader cursor, which the
1147
+ * partition-merged tail does not hold.
1148
+ */
1149
+ const seenByPartition = new Map();
1150
+ const gapCheck = (partition, expected, arrived) => journal.entries({
1151
+ runId: JournalEvent.RunId.make(partition),
1152
+ ...(expected === undefined ? {} : { after: JournalEvent.Seq.make(expected) }),
1153
+ limit: 1
1154
+ }).pipe(Effect.mapError(watchReadFailed), Effect.flatMap((page) => {
1155
+ const missed = page.entries[0];
1156
+ if (missed === undefined || missed.seq >= arrived)
1157
+ return Effect.void;
1158
+ return Effect.fail(new PersistenceError({
1159
+ operation: "watch",
1160
+ message: `the live tail lost journal entries for ${partition}: sequence ${missed.seq} was committed but never delivered to this watcher`
1161
+ }));
1162
+ }));
1163
+ const trackTail = (entry, cutoff) => {
1164
+ const partition = String(entry.runId);
1165
+ const expected = seenByPartition.get(partition) ?? cutoff;
1166
+ // A committed entry at or below the cursor was already delivered
1167
+ // (or is covered by the snapshot); passing it on teaches nothing.
1168
+ if (expected !== undefined && entry.seq <= expected)
1169
+ return Effect.succeed(Option.none());
1170
+ seenByPartition.set(partition, entry.seq);
1171
+ return expected !== undefined && entry.seq === expected + 1
1172
+ ? Effect.succeed(Option.some(entry))
1173
+ : Effect.map(gapCheck(partition, expected, entry.seq), () => Option.some(entry));
1174
+ };
1175
+ const tail = Stream.fromSubscription(subscription).pipe(
1176
+ // Every committed entry names its partition. The first one from a
1177
+ // partition nobody pinned yet pins it here and is preceded by that
1178
+ // partition's snapshot, which also recovers any earlier notice the
1179
+ // sliding buffer dropped.
1180
+ Stream.flatMap((entry) => Stream.unwrap(Effect.gen(function* () {
1181
+ const partition = String(entry.runId);
1182
+ const { highWater, claimed } = yield* cutoffFor(partition);
1183
+ const history = claimed ? snapshotForRunAt(partition, filter, highWater) : Stream.empty;
1184
+ if (highWater !== undefined && entry.seq <= highWater)
1185
+ return history;
1186
+ const tracked = yield* trackTail(entry, highWater);
1187
+ return Option.isSome(tracked)
1188
+ ? Stream.concat(history, Stream.succeed(eventFromEntry(tracked.value)))
1189
+ : history;
1190
+ }))));
1191
+ // The walk already bounds its own reads, and the tail is its own
1192
+ // fiber, so snapshot work never starves the live tail. An unbounded
1193
+ // merge read every partition of an unbounded database at once,
1194
+ // which is the allocation a remote watcher could force.
1195
+ return Stream.merge(walk, tail);
1196
+ }));
1197
+ /**
1198
+ * Keeps a restricted reader's watch to the runs it launched. A named run it
1199
+ * may not see fails `RunNotFound` exactly as a missing one does; a global
1200
+ * watch drops every other partition's events, deciding each partition once.
1201
+ */
1202
+ const watch = (filter) => {
1203
+ const { reader, ...unrestricted } = filter;
1204
+ if (reader === undefined)
1205
+ return watchAll(unrestricted);
1206
+ if (filter.runId !== undefined) {
1207
+ const runId = filter.runId;
1208
+ return Stream.unwrap(Effect.map(readerSees(reader, runId), (visible) => visible ? watchAll(unrestricted) : Stream.fail(new RunNotFound({ runId }))));
1209
+ }
1210
+ const decided = new Map();
1211
+ return watchAll(unrestricted).pipe(
1212
+ // A lost-tail failure names the partition and sequence it missed,
1213
+ // which may be another principal's; a restricted reader learns only
1214
+ // that its stream ended incomplete.
1215
+ Stream.mapError((error) => error._tag === "/control/PersistenceError"
1216
+ ? new PersistenceError({ operation: "watch", message: "the watch lost journal entries and ended" })
1217
+ : error), Stream.filterEffect((event) => {
1218
+ // eventFromEntry, Lineage.derive and Steering.derive retain the journal partition.
1219
+ const partition = event.runId;
1220
+ const known = decided.get(partition);
1221
+ if (known !== undefined)
1222
+ return Effect.succeed(known);
1223
+ return Effect.tap(readerSees(reader, partition), (visible) => Effect.sync(() => decided.set(partition, visible)));
1224
+ }));
1225
+ };
1226
+ /** Expands each source row in stable order and checkpoints individual members. */
1227
+ const watchAll = (filter) => {
1228
+ if (filter.afterSequence !== undefined && filter.runId === undefined) {
1229
+ return Stream.fail(invalid("afterSequence: a watch cursor resumes one run, so it requires runId"));
1230
+ }
1231
+ const cursor = filter.afterCursor;
1232
+ if (cursor !== undefined) {
1233
+ if (filter.runId === undefined) {
1234
+ return Stream.fail(invalid("afterCursor: a watch cursor resumes one run, so it requires runId"));
1235
+ }
1236
+ if (filter.afterSequence !== undefined) {
1237
+ return Stream.fail(invalid("afterCursor: cannot be combined with afterSequence"));
1238
+ }
1239
+ if (!Schema.is(WatchCursor)(cursor)) {
1240
+ return Stream.fail(invalid("afterCursor: sequence and offset must be nonnegative safe journal integers"));
1241
+ }
1242
+ }
1243
+ // A partial checkpoint rereads only its source row. A complete one
1244
+ // starts strictly after it, so polling never rereads completed entries.
1245
+ const afterSequence = cursor === undefined ?
1246
+ filter.afterSequence
1247
+ : cursor.offset === undefined ?
1248
+ cursor.sequence
1249
+ : cursor.sequence === 0
1250
+ ? undefined
1251
+ : cursor.sequence - 1;
1252
+ return entries({
1253
+ ...filter,
1254
+ afterSequence
1255
+ }).pipe(Stream.map((event) => {
1256
+ const lineage = Lineage.derive(event);
1257
+ const expanded = [event, ...(lineage === undefined ? [] : [lineage]), ...Steering.derive(event)];
1258
+ const checkpointed = expanded.map((member, offset) => ({
1259
+ ...member,
1260
+ cursor: offset === expanded.length - 1 ? { sequence: event.sequence } : { sequence: event.sequence, offset }
1261
+ }));
1262
+ return cursor?.offset !== undefined && event.sequence === cursor.sequence
1263
+ ? checkpointed.slice(cursor.offset + 1)
1264
+ : checkpointed;
1265
+ }), Stream.flattenIterable);
1266
+ };
1267
+ const service = {
1268
+ plan: Effect.fn("Control.plan")((input) => mutationSemaphore.withPermits(1)(journal.transact(Effect.gen(function* () {
1269
+ // The SQL runtime's plan, key and token writes join this transaction.
1270
+ // Memory publication cannot roll back, and older SQL writes could
1271
+ // commit without an entry, so a stored card also needs a journal check.
1272
+ const { card, created } = yield* runtime.plan(input);
1273
+ const runId = JournalEvent.RunId.make(`plan:${card.planId}`);
1274
+ if (!created) {
1275
+ let after;
1276
+ while (true) {
1277
+ const page = yield* journal.entries({
1278
+ runId,
1279
+ ...(after === undefined ? {} : { after }),
1280
+ limit: snapshotPageSize
1281
+ });
1282
+ if (page.entries.some((entry) => entry.sourceId === sourceId && entry.eventType === "control.plan.created")) {
1283
+ return card;
1284
+ }
1285
+ if (!page.hasMore)
1286
+ break;
1287
+ after = page.entries[page.entries.length - 1].seq;
1288
+ }
1289
+ }
1290
+ yield* emit(runId, "control.plan.created", {
1291
+ planId: card.planId,
1292
+ flowId: card.flowId,
1293
+ digest: card.digest
1294
+ });
1295
+ return card;
1296
+ })).pipe(Effect.mapError((cause) => cause instanceof Journal.JournalError
1297
+ ? new PersistenceError({
1298
+ operation: "plan",
1299
+ message: "Failed to commit plan and its creation entry atomically",
1300
+ cause
1301
+ })
1302
+ : cause))).pipe(Effect.flatMap((card) => registry.warnings().pipe(Effect.map((all) => {
1303
+ const warnings = all.filter((warning) => warning.name === input.flowId);
1304
+ return warnings.length === 0 ? card : { warnings, ...card };
1305
+ }))))),
1306
+ run: Effect.fn("Control.run")((submitted) => Effect.gen(function* () {
1307
+ const input = yield* snapshotRun(submitted);
1308
+ // One resume, one implementation. This member used to be a second
1309
+ // path with none of `Control.resume`'s fixes: it claimed without
1310
+ // `scope: "launched"`, so resuming an engine-created child overwrote
1311
+ // the engine's continuation state; it replayed a recorded receipt as
1312
+ // `AlreadyApplied` for a run that had since settled; and it journaled
1313
+ // `control.run.resumed`, which `AgentSession` reads as an approval
1314
+ // DELEGATION rather than as the claim a resume is.
1315
+ if (input._tag === "Resume")
1316
+ return yield* runMutation(input);
1317
+ const principal = yield* runtime.stampPrincipal(input.principal);
1318
+ let admitted;
1319
+ const receipt = yield* mutate("run", input.idempotencyKey, principal, fingerprint("run", principal, input), Effect.gen(function* () {
1320
+ const launched = yield* runtime.launch(input.planId, input.digest, input.envelope, principal, input.reservedRunId);
1321
+ if (launched._tag === "Parked") {
1322
+ return { ...launched.receipt, receiptId: input.idempotencyKey };
1323
+ }
1324
+ const plan = yield* runtime.getPlan(input.planId);
1325
+ const trigger = declaredTrigger(plan.decodedInput);
1326
+ yield* emit(launched.run.runId, "control.run.accepted", {
1327
+ runId: launched.run.runId,
1328
+ planId: input.planId,
1329
+ digest: input.digest,
1330
+ status: launched.run.status,
1331
+ ...(trigger === undefined ? {} : { trigger }),
1332
+ ...ControlFacts.runFact(launched.run, "created")
1333
+ });
1334
+ admitted = { plan, run: launched.run };
1335
+ return {
1336
+ _tag: "Accepted",
1337
+ receiptId: input.idempotencyKey,
1338
+ runId: launched.run.runId
1339
+ };
1340
+ }), true, true);
1341
+ // The executor may immediately read through another connection or
1342
+ // fork a driver. Its run, approval and dedupe receipt must all be
1343
+ // committed before any execution crosses that boundary.
1344
+ if (receipt._tag === "Accepted" && admitted !== undefined)
1345
+ yield* hand(admitted);
1346
+ else if (receipt._tag === "AlreadyApplied" && receipt.runId !== undefined) {
1347
+ yield* relaunchStranded(receipt.runId);
1348
+ }
1349
+ return receipt;
1350
+ })),
1351
+ approve: Effect.fn("Control.approve")((input) => decide("approved", input)),
1352
+ deny: Effect.fn("Control.deny")((input) => decide("denied", input)),
1353
+ steer: Effect.fn("Control.steer")((submitted) => confined(submitted, Effect.flatMap(snapshotSteer(submitted), (input) => mutate("steer", input.idempotencyKey, input.message.principal, fingerprint("steer", input.message.principal, input), Effect.gen(function* () {
1354
+ // Two run ids naming two runs is a caller mistake with a durable
1355
+ // consequence: the notification is admitted to `input.runId` while
1356
+ // the stored `SteerMessage.runId` names another run, so the message
1357
+ // an operator later reads says it belongs somewhere it was never
1358
+ // delivered.
1359
+ if (input.message.runId !== input.runId) {
1360
+ return yield* Effect.fail(invalid(`message.runId: must be ${JSON.stringify(input.runId)}, received ${JSON.stringify(input.message.runId)}`));
1361
+ }
1362
+ const run = yield* getRun(input.runId);
1363
+ // A run that will never take another turn cannot be steered, and
1364
+ // storing the steer anyway would leave an operator watching a
1365
+ // message that has no boundary left to deliver it.
1366
+ if (terminal(run.status))
1367
+ return { _tag: "Terminal", runId: run.runId, status: run.status };
1368
+ const item = steerItem(input.message);
1369
+ const admission = yield* notifications.admit(input.runId, {
1370
+ _tag: "human-steer",
1371
+ id: input.message.messageId,
1372
+ delivery: "steer",
1373
+ targetLineageId: input.runId,
1374
+ provenance: {
1375
+ sourceRunId: input.runId,
1376
+ sourceLineageId: input.runId,
1377
+ sourceTurn: 0,
1378
+ sourceActor: `${input.message.principal.kind}:${input.message.principal.id}`,
1379
+ ...(input.message.attribution === undefined ? {} : { attribution: input.message.attribution })
1380
+ },
1381
+ payload: SteerPayload.encode(item)
1382
+ }, input.version).pipe(Effect.mapError((cause) => cause instanceof NotificationQueue.NotificationError ? cause : new PersistenceError({
1383
+ operation: "control.steer.notification",
1384
+ message: "Failed to admit steering notification",
1385
+ cause
1386
+ })));
1387
+ const inputReceipt = () => {
1388
+ if (input.version === undefined || item.kind !== "Message") {
1389
+ return accepted(input.idempotencyKey, input.runId);
1390
+ }
1391
+ const payload = admission.consumedNotification?.payload;
1392
+ return {
1393
+ _tag: "Accepted",
1394
+ receiptId: input.idempotencyKey,
1395
+ runId: input.runId,
1396
+ inputConsumed: admission.consumed === true,
1397
+ inputBody: payload?.body ?? item.body
1398
+ };
1399
+ };
1400
+ if (input.version !== undefined && (admission.consumed || admission.duplicate)) {
1401
+ return inputReceipt();
1402
+ }
1403
+ if (admission.decision === "rejected-full") {
1404
+ return yield* Effect.fail(new NotificationQueue.NotificationError({
1405
+ code: "notification_full",
1406
+ notificationId: input.message.messageId,
1407
+ message: "Steering queue is full; retry after pending notifications are delivered"
1408
+ }));
1409
+ }
1410
+ // `createdAt` is the caller's own stated time, and the enqueue
1411
+ // entry is the one place it is kept: `steerItem` strips the control
1412
+ // envelope before the message reaches the queue, so a field the
1413
+ // journal did not carry was a field nothing ever read.
1414
+ yield* emit(input.runId, Steering.enqueuedEventType, {
1415
+ runId: input.runId,
1416
+ messageId: input.message.messageId,
1417
+ kind: item.kind,
1418
+ createdAt: input.message.createdAt
1419
+ });
1420
+ yield* wake(run, input.message.messageId);
1421
+ return inputReceipt();
1422
+ }))))),
1423
+ signal: Effect.fn("Control.signal")((submitted) => confined(submitted, Effect.flatMap(snapshotSignal(submitted), (input) => Effect.gen(function* () {
1424
+ const principal = yield* runtime.stampPrincipal(input.principal);
1425
+ const key = fingerprint("signal", principal, input);
1426
+ const durableKey = mutationKey("signal", input.idempotencyKey, principal);
1427
+ // Admission, payload and receipt commit together before any engine
1428
+ // operation. The writer is released before completing a deferred.
1429
+ const receipt = yield* mutate("signal", input.idempotencyKey, principal, key, Effect.gen(function* () {
1430
+ const current = yield* getRun(input.runId);
1431
+ if (terminal(current.status)) {
1432
+ return { _tag: "Terminal", runId: current.runId, status: current.status };
1433
+ }
1434
+ // The admitting identity is stored with the command: whether the
1435
+ // signal may answer a human wait is decided at delivery, which
1436
+ // can be a replay after restart with no caller present.
1437
+ yield* runtime.admitSignal(durableKey, input.runId, input.signal, principal);
1438
+ yield* emit(input.runId, "control.signal.admitted", {
1439
+ commandId: durableKey,
1440
+ runId: input.runId,
1441
+ name: input.signal.name
1442
+ });
1443
+ return accepted(input.idempotencyKey, input.runId);
1444
+ }), true, true);
1445
+ if (receipt._tag !== "Accepted" && receipt._tag !== "AlreadyApplied")
1446
+ return receipt;
1447
+ const command = yield* runtime.signalCommand(durableKey);
1448
+ if (command === undefined || command.state === "delivered" || command.state === "terminal")
1449
+ return receipt;
1450
+ if (command.state === "rejected") {
1451
+ return yield* new NoMatchingWait({ runId: input.runId, waitName: input.signal.name });
1452
+ }
1453
+ const delivery = Option.isNone(executor) ?
1454
+ "unknown"
1455
+ : yield* executor.value.deliverSignal({ ...command });
1456
+ if (delivery === "no-match") {
1457
+ yield* runtime.settleSignal(durableKey, "rejected");
1458
+ return yield* new NoMatchingWait({ runId: input.runId, waitName: input.signal.name });
1459
+ }
1460
+ if (delivery === "refused") {
1461
+ yield* runtime.settleSignal(durableKey, "rejected");
1462
+ return yield* new Unauthorized({
1463
+ message: `This caller has no authority to answer "${input.signal.name}" on run ${input.runId}`
1464
+ });
1465
+ }
1466
+ if (delivery === "delivered") {
1467
+ yield* runtime.settleSignal(durableKey, "delivered");
1468
+ }
1469
+ return receipt;
1470
+ })))),
1471
+ cancel: Effect.fn("Control.cancel")((submitted) => confined(submitted, Effect.flatMap(snapshotReasonedMutation("cancel", submitted), (input) => Effect.flatMap(runtime.stampPrincipal(input.principal), (principal) => mutate("cancel", input.idempotencyKey, principal, fingerprint("cancel", principal, input), Effect.gen(function* () {
1472
+ const current = yield* getRun(input.runId);
1473
+ // A run that has already settled cannot be cancelled, and a cancel
1474
+ // request journaled against it would be a request nothing can ever
1475
+ // act on. Answer with what actually happened to the run.
1476
+ if (terminal(current.status)) {
1477
+ yield* reconcileTerminal(input.runId, current.status);
1478
+ return { _tag: "Terminal", runId: current.runId, status: current.status };
1479
+ }
1480
+ // The durable half, and the only half that reaches a run another
1481
+ // process owns: fibers are process-local, so an interrupt can only
1482
+ // stop a run this process is driving. The executor writes
1483
+ // `cancel_requested_at_ms` on the engine row instead, and the
1484
+ // owner's cancel poll acts on it within a heartbeat.
1485
+ //
1486
+ // It runs INSIDE the mutation's transaction on purpose. An engine
1487
+ // that refuses the request rolls the whole cancel back — no
1488
+ // attribution event, no terminal control status — because a
1489
+ // control row that says `cancelled` while the engine row is still
1490
+ // running is the one state an operator can never recover from.
1491
+ //
1492
+ // It runs BEFORE the attribution event for the mirror-image
1493
+ // reason. The control row this plane read may be stale — the two
1494
+ // `flows_runs` tables are two files in the shipped CLI — and an
1495
+ // engine row that has already settled makes the cancel a request
1496
+ // nobody can act on. Attributing and transitioning it anyway is
1497
+ // exactly the terminal disagreement B-11 forbids, so the engine's
1498
+ // own status becomes the receipt and nothing else happens.
1499
+ const record = yield* executorRequestCancel(input.runId);
1500
+ if (typeof record !== "string") {
1501
+ // The engine finished the run before the request arrived. Nobody
1502
+ // cancelled anything, so no attribution is written; but leaving
1503
+ // the control row saying `running` for a run the engine settled
1504
+ // is permanent, because no verb converges it: `cancel` answers
1505
+ // `Terminal` without writing and `resume` refuses a settled run.
1506
+ // `ps` listed it live and `gc` skipped it forever. Writing the
1507
+ // ENGINE's own status is convergence, not the terminal
1508
+ // disagreement B-11 forbids, which is a control row reading
1509
+ // `cancelled` over an engine row reading `completed`.
1510
+ yield* reconcileTerminal(input.runId, record.status);
1511
+ return { _tag: "Terminal", runId: input.runId, status: record.status };
1512
+ }
1513
+ // Attribution is keyed on the request being NEWLY recorded. A
1514
+ // cancel that committed without it would be durable and anonymous,
1515
+ // and nothing afterwards could say who asked — but this mutation
1516
+ // runs with `replay: false`, so an operator asking a second time
1517
+ // re-executes it, and attributing every ask journaled one
1518
+ // `control.run.cancel-requested` per ask for one cancellation.
1519
+ // `already-requested` is the engine saying the column was set
1520
+ // before this call arrived, so the record already exists.
1521
+ //
1522
+ // It stays BEFORE the interrupt, and in the mutation's own
1523
+ // transaction.
1524
+ const prior = yield* runtime.lookupMutation(mutationKey("cancel", input.idempotencyKey, principal), fingerprint("cancel", principal, input));
1525
+ // A retry after cleanup or settlement failed already committed
1526
+ // this request's attribution with its acceptance receipt.
1527
+ if (record !== "already-requested" && prior === undefined) {
1528
+ yield* emit(input.runId, Cancellation.requestedEventType, json({
1529
+ runId: input.runId,
1530
+ source: "control",
1531
+ principal,
1532
+ ...(input.reason === undefined ? {} : { reason: input.reason })
1533
+ }));
1534
+ }
1535
+ return accepted(input.idempotencyKey, input.runId);
1536
+ }), false).pipe(Effect.flatMap((receipt) => Effect.gen(function* () {
1537
+ if (receipt._tag !== "Accepted")
1538
+ return receipt;
1539
+ // The request and receipt are committed before any finalizer runs.
1540
+ // Finalizers may use this same mutation permit or durable writer.
1541
+ const settle = (effect) => transact("cancel", Effect.gen(function* () {
1542
+ const run = yield* effect;
1543
+ yield* emit(input.runId, `control.run.${run.status}`, {
1544
+ runId: input.runId,
1545
+ status: run.status,
1546
+ ...ControlFacts.runFact(run)
1547
+ });
1548
+ return run;
1549
+ }));
1550
+ const run = yield* runtime.interrupt(input.runId, settle).pipe(Effect.catchTag("/control/ClaimLost", () => Effect.gen(function* () {
1551
+ const current = yield* getRun(input.runId);
1552
+ if (terminal(current.status))
1553
+ return current;
1554
+ // A live peer acts on the durable request. An unowned park
1555
+ // needs this caller to claim it and finish the cancellation.
1556
+ if (live(current.status) && current.ownerId !== undefined)
1557
+ return undefined;
1558
+ return yield* runtime.resume(input.runId).pipe(Effect.andThen(runtime.interrupt(input.runId, settle)), Effect.catchTag("/control/ClaimLost", () => Effect.succeed(undefined)));
1559
+ })));
1560
+ return run === undefined
1561
+ ? receipt
1562
+ : terminalOrAccepted(input.idempotencyKey, run);
1563
+ })),
1564
+ // Both rows, before the process that asked goes away. The engine row
1565
+ // carries the request the moment the mutation commits, but nothing
1566
+ // drives a parked run, so the row stayed `suspended` until some
1567
+ // later long-lived engine happened to sweep it: `gc` collected the
1568
+ // run in `control.db` and skipped it in `engine.db` for fifteen
1569
+ // seconds and six commands in the release validation.
1570
+ Effect.tap(() => Effect.gen(function* () {
1571
+ yield* executorSettleCancelledPark(input.runId);
1572
+ // The engine can finish between the request and local interrupt,
1573
+ // or while settling an unowned park. Its read overlay alone does
1574
+ // not persist the control row or deliver the terminal watch event.
1575
+ const current = yield* getRun(input.runId);
1576
+ if (terminal(current.status))
1577
+ yield* reconcileTerminal(input.runId, current.status);
1578
+ }))))))),
1579
+ resume: Effect.fn("Control.resume")((input) => confined(input, runMutation(input))),
1580
+ list,
1581
+ watch
1582
+ };
1583
+ return Control.of(service);
1584
+ }));
1585
+ //# sourceMappingURL=ControlLive.js.map