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

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