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