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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (418) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/LICENSE +21 -0
  3. package/README.md +168 -2
  4. package/dist/cjs/ApprovalAuthority.d.ts +73 -0
  5. package/dist/cjs/ApprovalAuthority.d.ts.map +1 -0
  6. package/dist/cjs/ApprovalAuthority.js +62 -0
  7. package/dist/cjs/ApprovalAuthority.js.map +7 -0
  8. package/dist/cjs/Cancellation.d.ts +107 -0
  9. package/dist/cjs/Cancellation.d.ts.map +1 -0
  10. package/dist/cjs/Cancellation.js +72 -0
  11. package/dist/cjs/Cancellation.js.map +7 -0
  12. package/dist/cjs/Channels.d.ts +170 -0
  13. package/dist/cjs/Channels.d.ts.map +1 -0
  14. package/dist/cjs/Channels.js +278 -0
  15. package/dist/cjs/Channels.js.map +7 -0
  16. package/dist/cjs/Control.d.ts +202 -0
  17. package/dist/cjs/Control.d.ts.map +1 -0
  18. package/dist/cjs/Control.js +47 -0
  19. package/dist/cjs/Control.js.map +7 -0
  20. package/dist/cjs/ControlClient.d.ts +52 -0
  21. package/dist/cjs/ControlClient.d.ts.map +1 -0
  22. package/dist/cjs/ControlClient.js +191 -0
  23. package/dist/cjs/ControlClient.js.map +7 -0
  24. package/dist/cjs/ControlError.d.ts +318 -0
  25. package/dist/cjs/ControlError.d.ts.map +1 -0
  26. package/dist/cjs/ControlError.js +249 -0
  27. package/dist/cjs/ControlError.js.map +7 -0
  28. package/dist/cjs/ControlExecutor.d.ts +372 -0
  29. package/dist/cjs/ControlExecutor.d.ts.map +1 -0
  30. package/dist/cjs/ControlExecutor.js +123 -0
  31. package/dist/cjs/ControlExecutor.js.map +7 -0
  32. package/dist/cjs/ControlFacts.d.ts +454 -0
  33. package/dist/cjs/ControlFacts.d.ts.map +1 -0
  34. package/dist/cjs/ControlFacts.js +261 -0
  35. package/dist/cjs/ControlFacts.js.map +7 -0
  36. package/dist/cjs/ControlLive.d.ts +23 -0
  37. package/dist/cjs/ControlLive.d.ts.map +1 -0
  38. package/dist/cjs/ControlLive.js +1280 -0
  39. package/dist/cjs/ControlLive.js.map +7 -0
  40. package/dist/cjs/ControlRpcs.d.ts +1204 -0
  41. package/dist/cjs/ControlRpcs.d.ts.map +1 -0
  42. package/dist/cjs/ControlRpcs.js +247 -0
  43. package/dist/cjs/ControlRpcs.js.map +7 -0
  44. package/dist/cjs/ControlRuntime.d.ts +635 -0
  45. package/dist/cjs/ControlRuntime.d.ts.map +1 -0
  46. package/dist/cjs/ControlRuntime.js +740 -0
  47. package/dist/cjs/ControlRuntime.js.map +7 -0
  48. package/dist/cjs/ControlSchema.d.ts +2642 -0
  49. package/dist/cjs/ControlSchema.d.ts.map +1 -0
  50. package/dist/cjs/ControlSchema.js +634 -0
  51. package/dist/cjs/ControlSchema.js.map +7 -0
  52. package/dist/cjs/ControlServer.d.ts +51 -0
  53. package/dist/cjs/ControlServer.d.ts.map +1 -0
  54. package/dist/cjs/ControlServer.js +121 -0
  55. package/dist/cjs/ControlServer.js.map +7 -0
  56. package/dist/cjs/Credential.d.ts +136 -0
  57. package/dist/cjs/Credential.d.ts.map +1 -0
  58. package/dist/cjs/Credential.js +168 -0
  59. package/dist/cjs/Credential.js.map +7 -0
  60. package/dist/cjs/CredentialCipher.d.ts +90 -0
  61. package/dist/cjs/CredentialCipher.d.ts.map +1 -0
  62. package/dist/cjs/CredentialCipher.js +45 -0
  63. package/dist/cjs/CredentialCipher.js.map +7 -0
  64. package/dist/cjs/CredentialStore.d.ts +97 -0
  65. package/dist/cjs/CredentialStore.d.ts.map +1 -0
  66. package/dist/cjs/CredentialStore.js +81 -0
  67. package/dist/cjs/CredentialStore.js.map +7 -0
  68. package/dist/cjs/DispatchReader.d.ts +112 -0
  69. package/dist/cjs/DispatchReader.d.ts.map +1 -0
  70. package/dist/cjs/DispatchReader.js +45 -0
  71. package/dist/cjs/DispatchReader.js.map +7 -0
  72. package/dist/cjs/Health.d.ts +333 -0
  73. package/dist/cjs/Health.d.ts.map +1 -0
  74. package/dist/cjs/Health.js +311 -0
  75. package/dist/cjs/Health.js.map +7 -0
  76. package/dist/cjs/JevSessionChecker.d.ts +57 -0
  77. package/dist/cjs/JevSessionChecker.d.ts.map +1 -0
  78. package/dist/cjs/JevSessionChecker.js +113 -0
  79. package/dist/cjs/JevSessionChecker.js.map +7 -0
  80. package/dist/cjs/Lineage.d.ts +131 -0
  81. package/dist/cjs/Lineage.d.ts.map +1 -0
  82. package/dist/cjs/Lineage.js +81 -0
  83. package/dist/cjs/Lineage.js.map +7 -0
  84. package/dist/cjs/Migrations.d.ts +34 -0
  85. package/dist/cjs/Migrations.d.ts.map +1 -0
  86. package/dist/cjs/Migrations.js +60 -0
  87. package/dist/cjs/Migrations.js.map +7 -0
  88. package/dist/cjs/Monitor.d.ts +282 -0
  89. package/dist/cjs/Monitor.d.ts.map +1 -0
  90. package/dist/cjs/Monitor.js +283 -0
  91. package/dist/cjs/Monitor.js.map +7 -0
  92. package/dist/cjs/ScopedToken.d.ts +193 -0
  93. package/dist/cjs/ScopedToken.d.ts.map +1 -0
  94. package/dist/cjs/ScopedToken.js +135 -0
  95. package/dist/cjs/ScopedToken.js.map +7 -0
  96. package/dist/cjs/SqlControlRuntime.d.ts +161 -0
  97. package/dist/cjs/SqlControlRuntime.d.ts.map +1 -0
  98. package/dist/cjs/SqlControlRuntime.js +1521 -0
  99. package/dist/cjs/SqlControlRuntime.js.map +7 -0
  100. package/dist/cjs/SqlCredentialStore.d.ts +43 -0
  101. package/dist/cjs/SqlCredentialStore.d.ts.map +1 -0
  102. package/dist/cjs/SqlCredentialStore.js +113 -0
  103. package/dist/cjs/SqlCredentialStore.js.map +7 -0
  104. package/dist/cjs/Steering.d.ts +69 -0
  105. package/dist/cjs/Steering.d.ts.map +1 -0
  106. package/dist/cjs/Steering.js +49 -0
  107. package/dist/cjs/Steering.js.map +7 -0
  108. package/dist/cjs/SystemFlows.d.ts +223 -0
  109. package/dist/cjs/SystemFlows.d.ts.map +1 -0
  110. package/dist/cjs/SystemFlows.js +195 -0
  111. package/dist/cjs/SystemFlows.js.map +7 -0
  112. package/dist/cjs/WebCryptoCipher.d.ts +49 -0
  113. package/dist/cjs/WebCryptoCipher.d.ts.map +1 -0
  114. package/dist/cjs/WebCryptoCipher.js +129 -0
  115. package/dist/cjs/WebCryptoCipher.js.map +7 -0
  116. package/dist/cjs/WebhookChannel.d.ts +113 -0
  117. package/dist/cjs/WebhookChannel.d.ts.map +1 -0
  118. package/dist/cjs/WebhookChannel.js +98 -0
  119. package/dist/cjs/WebhookChannel.js.map +7 -0
  120. package/dist/cjs/index.d.ts +160 -0
  121. package/dist/cjs/index.d.ts.map +1 -0
  122. package/dist/cjs/index.js +91 -0
  123. package/dist/cjs/index.js.map +7 -0
  124. package/dist/cjs/internal/MutationBoundary.d.ts +27 -0
  125. package/dist/cjs/internal/MutationBoundary.d.ts.map +1 -0
  126. package/dist/cjs/internal/MutationBoundary.js +50 -0
  127. package/dist/cjs/internal/MutationBoundary.js.map +7 -0
  128. package/dist/cjs/internal/activeFibers.d.ts +12 -0
  129. package/dist/cjs/internal/activeFibers.d.ts.map +1 -0
  130. package/dist/cjs/internal/activeFibers.js +30 -0
  131. package/dist/cjs/internal/activeFibers.js.map +7 -0
  132. package/dist/cjs/internal/issues.d.ts +28 -0
  133. package/dist/cjs/internal/issues.d.ts.map +1 -0
  134. package/dist/cjs/internal/issues.js +34 -0
  135. package/dist/cjs/internal/issues.js.map +7 -0
  136. package/dist/cjs/internal/planning.d.ts +347 -0
  137. package/dist/cjs/internal/planning.d.ts.map +1 -0
  138. package/dist/cjs/internal/planning.js +137 -0
  139. package/dist/cjs/internal/planning.js.map +7 -0
  140. package/dist/cjs/internal/sqlSchemaErrors.d.ts +18 -0
  141. package/dist/cjs/internal/sqlSchemaErrors.d.ts.map +1 -0
  142. package/dist/cjs/internal/sqlSchemaErrors.js +40 -0
  143. package/dist/cjs/internal/sqlSchemaErrors.js.map +7 -0
  144. package/dist/cjs/migrations/0001_control_tables.d.ts +18 -0
  145. package/dist/cjs/migrations/0001_control_tables.d.ts.map +1 -0
  146. package/dist/cjs/migrations/0001_control_tables.js +115 -0
  147. package/dist/cjs/migrations/0001_control_tables.js.map +7 -0
  148. package/dist/cjs/migrations/0002_run_keys.d.ts +15 -0
  149. package/dist/cjs/migrations/0002_run_keys.d.ts.map +1 -0
  150. package/dist/cjs/migrations/0002_run_keys.js +44 -0
  151. package/dist/cjs/migrations/0002_run_keys.js.map +7 -0
  152. package/dist/cjs/migrations/0003_signal_commands.d.ts +13 -0
  153. package/dist/cjs/migrations/0003_signal_commands.d.ts.map +1 -0
  154. package/dist/cjs/migrations/0003_signal_commands.js +50 -0
  155. package/dist/cjs/migrations/0003_signal_commands.js.map +7 -0
  156. package/dist/cjs/migrations/0004_approval_decisions.d.ts +13 -0
  157. package/dist/cjs/migrations/0004_approval_decisions.d.ts.map +1 -0
  158. package/dist/cjs/migrations/0004_approval_decisions.js +45 -0
  159. package/dist/cjs/migrations/0004_approval_decisions.js.map +7 -0
  160. package/dist/cjs/migrations/0005_signal_principals.d.ts +18 -0
  161. package/dist/cjs/migrations/0005_signal_principals.d.ts.map +1 -0
  162. package/dist/cjs/migrations/0005_signal_principals.js +45 -0
  163. package/dist/cjs/migrations/0005_signal_principals.js.map +7 -0
  164. package/dist/cjs/migrations/0006_run_principals.d.ts +18 -0
  165. package/dist/cjs/migrations/0006_run_principals.d.ts.map +1 -0
  166. package/dist/cjs/migrations/0006_run_principals.js +49 -0
  167. package/dist/cjs/migrations/0006_run_principals.js.map +7 -0
  168. package/dist/cjs/migrations/0007_resume_consent.d.ts +18 -0
  169. package/dist/cjs/migrations/0007_resume_consent.d.ts.map +1 -0
  170. package/dist/cjs/migrations/0007_resume_consent.js +46 -0
  171. package/dist/cjs/migrations/0007_resume_consent.js.map +7 -0
  172. package/dist/cjs/package.json +1 -0
  173. package/dist/cjs/test/TestControl.d.ts +19 -0
  174. package/dist/cjs/test/TestControl.d.ts.map +1 -0
  175. package/dist/cjs/test/TestControl.js +62 -0
  176. package/dist/cjs/test/TestControl.js.map +7 -0
  177. package/dist/esm/ApprovalAuthority.d.ts +73 -0
  178. package/dist/esm/ApprovalAuthority.d.ts.map +1 -0
  179. package/dist/esm/ApprovalAuthority.js +72 -0
  180. package/dist/esm/ApprovalAuthority.js.map +1 -0
  181. package/dist/esm/Cancellation.d.ts +107 -0
  182. package/dist/esm/Cancellation.d.ts.map +1 -0
  183. package/dist/esm/Cancellation.js +116 -0
  184. package/dist/esm/Cancellation.js.map +1 -0
  185. package/dist/esm/Channels.d.ts +170 -0
  186. package/dist/esm/Channels.d.ts.map +1 -0
  187. package/dist/esm/Channels.js +312 -0
  188. package/dist/esm/Channels.js.map +1 -0
  189. package/dist/esm/Control.d.ts +202 -0
  190. package/dist/esm/Control.d.ts.map +1 -0
  191. package/dist/esm/Control.js +42 -0
  192. package/dist/esm/Control.js.map +1 -0
  193. package/dist/esm/ControlClient.d.ts +52 -0
  194. package/dist/esm/ControlClient.d.ts.map +1 -0
  195. package/dist/esm/ControlClient.js +217 -0
  196. package/dist/esm/ControlClient.js.map +1 -0
  197. package/dist/esm/ControlError.d.ts +318 -0
  198. package/dist/esm/ControlError.d.ts.map +1 -0
  199. package/dist/esm/ControlError.js +359 -0
  200. package/dist/esm/ControlError.js.map +1 -0
  201. package/dist/esm/ControlExecutor.d.ts +372 -0
  202. package/dist/esm/ControlExecutor.d.ts.map +1 -0
  203. package/dist/esm/ControlExecutor.js +212 -0
  204. package/dist/esm/ControlExecutor.js.map +1 -0
  205. package/dist/esm/ControlFacts.d.ts +454 -0
  206. package/dist/esm/ControlFacts.d.ts.map +1 -0
  207. package/dist/esm/ControlFacts.js +324 -0
  208. package/dist/esm/ControlFacts.js.map +1 -0
  209. package/dist/esm/ControlLive.d.ts +23 -0
  210. package/dist/esm/ControlLive.d.ts.map +1 -0
  211. package/dist/esm/ControlLive.js +1585 -0
  212. package/dist/esm/ControlLive.js.map +1 -0
  213. package/dist/esm/ControlRpcs.d.ts +1204 -0
  214. package/dist/esm/ControlRpcs.d.ts.map +1 -0
  215. package/dist/esm/ControlRpcs.js +299 -0
  216. package/dist/esm/ControlRpcs.js.map +1 -0
  217. package/dist/esm/ControlRuntime.d.ts +635 -0
  218. package/dist/esm/ControlRuntime.d.ts.map +1 -0
  219. package/dist/esm/ControlRuntime.js +807 -0
  220. package/dist/esm/ControlRuntime.js.map +1 -0
  221. package/dist/esm/ControlSchema.d.ts +2642 -0
  222. package/dist/esm/ControlSchema.d.ts.map +1 -0
  223. package/dist/esm/ControlSchema.js +1030 -0
  224. package/dist/esm/ControlSchema.js.map +1 -0
  225. package/dist/esm/ControlServer.d.ts +51 -0
  226. package/dist/esm/ControlServer.d.ts.map +1 -0
  227. package/dist/esm/ControlServer.js +145 -0
  228. package/dist/esm/ControlServer.js.map +1 -0
  229. package/dist/esm/Credential.d.ts +136 -0
  230. package/dist/esm/Credential.d.ts.map +1 -0
  231. package/dist/esm/Credential.js +190 -0
  232. package/dist/esm/Credential.js.map +1 -0
  233. package/dist/esm/CredentialCipher.d.ts +90 -0
  234. package/dist/esm/CredentialCipher.d.ts.map +1 -0
  235. package/dist/esm/CredentialCipher.js +56 -0
  236. package/dist/esm/CredentialCipher.js.map +1 -0
  237. package/dist/esm/CredentialStore.d.ts +97 -0
  238. package/dist/esm/CredentialStore.d.ts.map +1 -0
  239. package/dist/esm/CredentialStore.js +101 -0
  240. package/dist/esm/CredentialStore.js.map +1 -0
  241. package/dist/esm/DispatchReader.d.ts +112 -0
  242. package/dist/esm/DispatchReader.d.ts.map +1 -0
  243. package/dist/esm/DispatchReader.js +76 -0
  244. package/dist/esm/DispatchReader.js.map +1 -0
  245. package/dist/esm/Health.d.ts +333 -0
  246. package/dist/esm/Health.d.ts.map +1 -0
  247. package/dist/esm/Health.js +400 -0
  248. package/dist/esm/Health.js.map +1 -0
  249. package/dist/esm/JevSessionChecker.d.ts +57 -0
  250. package/dist/esm/JevSessionChecker.d.ts.map +1 -0
  251. package/dist/esm/JevSessionChecker.js +108 -0
  252. package/dist/esm/JevSessionChecker.js.map +1 -0
  253. package/dist/esm/Lineage.d.ts +131 -0
  254. package/dist/esm/Lineage.d.ts.map +1 -0
  255. package/dist/esm/Lineage.js +174 -0
  256. package/dist/esm/Lineage.js.map +1 -0
  257. package/dist/esm/Migrations.d.ts +34 -0
  258. package/dist/esm/Migrations.d.ts.map +1 -0
  259. package/dist/esm/Migrations.js +53 -0
  260. package/dist/esm/Migrations.js.map +1 -0
  261. package/dist/esm/Monitor.d.ts +282 -0
  262. package/dist/esm/Monitor.d.ts.map +1 -0
  263. package/dist/esm/Monitor.js +415 -0
  264. package/dist/esm/Monitor.js.map +1 -0
  265. package/dist/esm/ScopedToken.d.ts +193 -0
  266. package/dist/esm/ScopedToken.d.ts.map +1 -0
  267. package/dist/esm/ScopedToken.js +224 -0
  268. package/dist/esm/ScopedToken.js.map +1 -0
  269. package/dist/esm/SqlControlRuntime.d.ts +161 -0
  270. package/dist/esm/SqlControlRuntime.d.ts.map +1 -0
  271. package/dist/esm/SqlControlRuntime.js +1756 -0
  272. package/dist/esm/SqlControlRuntime.js.map +1 -0
  273. package/dist/esm/SqlCredentialStore.d.ts +43 -0
  274. package/dist/esm/SqlCredentialStore.d.ts.map +1 -0
  275. package/dist/esm/SqlCredentialStore.js +97 -0
  276. package/dist/esm/SqlCredentialStore.js.map +1 -0
  277. package/dist/esm/Steering.d.ts +69 -0
  278. package/dist/esm/Steering.d.ts.map +1 -0
  279. package/dist/esm/Steering.js +89 -0
  280. package/dist/esm/Steering.js.map +1 -0
  281. package/dist/esm/SystemFlows.d.ts +223 -0
  282. package/dist/esm/SystemFlows.d.ts.map +1 -0
  283. package/dist/esm/SystemFlows.js +198 -0
  284. package/dist/esm/SystemFlows.js.map +1 -0
  285. package/dist/esm/WebCryptoCipher.d.ts +49 -0
  286. package/dist/esm/WebCryptoCipher.d.ts.map +1 -0
  287. package/dist/esm/WebCryptoCipher.js +123 -0
  288. package/dist/esm/WebCryptoCipher.js.map +1 -0
  289. package/dist/esm/WebhookChannel.d.ts +113 -0
  290. package/dist/esm/WebhookChannel.d.ts.map +1 -0
  291. package/dist/esm/WebhookChannel.js +109 -0
  292. package/dist/esm/WebhookChannel.js.map +1 -0
  293. package/dist/esm/index.d.ts +160 -0
  294. package/dist/esm/index.d.ts.map +1 -0
  295. package/dist/esm/index.js +160 -0
  296. package/dist/esm/index.js.map +1 -0
  297. package/dist/esm/internal/MutationBoundary.d.ts +27 -0
  298. package/dist/esm/internal/MutationBoundary.d.ts.map +1 -0
  299. package/dist/esm/internal/MutationBoundary.js +40 -0
  300. package/dist/esm/internal/MutationBoundary.js.map +1 -0
  301. package/dist/esm/internal/activeFibers.d.ts +12 -0
  302. package/dist/esm/internal/activeFibers.d.ts.map +1 -0
  303. package/dist/esm/internal/activeFibers.js +17 -0
  304. package/dist/esm/internal/activeFibers.js.map +1 -0
  305. package/dist/esm/internal/issues.d.ts +28 -0
  306. package/dist/esm/internal/issues.d.ts.map +1 -0
  307. package/dist/esm/internal/issues.js +35 -0
  308. package/dist/esm/internal/issues.js.map +1 -0
  309. package/dist/esm/internal/planning.d.ts +347 -0
  310. package/dist/esm/internal/planning.d.ts.map +1 -0
  311. package/dist/esm/internal/planning.js +199 -0
  312. package/dist/esm/internal/planning.js.map +1 -0
  313. package/dist/esm/internal/sqlSchemaErrors.d.ts +18 -0
  314. package/dist/esm/internal/sqlSchemaErrors.d.ts.map +1 -0
  315. package/dist/esm/internal/sqlSchemaErrors.js +38 -0
  316. package/dist/esm/internal/sqlSchemaErrors.js.map +1 -0
  317. package/dist/esm/migrations/0001_control_tables.d.ts +18 -0
  318. package/dist/esm/migrations/0001_control_tables.d.ts.map +1 -0
  319. package/dist/esm/migrations/0001_control_tables.js +96 -0
  320. package/dist/esm/migrations/0001_control_tables.js.map +1 -0
  321. package/dist/esm/migrations/0002_run_keys.d.ts +15 -0
  322. package/dist/esm/migrations/0002_run_keys.d.ts.map +1 -0
  323. package/dist/esm/migrations/0002_run_keys.js +22 -0
  324. package/dist/esm/migrations/0002_run_keys.js.map +1 -0
  325. package/dist/esm/migrations/0003_signal_commands.d.ts +13 -0
  326. package/dist/esm/migrations/0003_signal_commands.d.ts.map +1 -0
  327. package/dist/esm/migrations/0003_signal_commands.js +26 -0
  328. package/dist/esm/migrations/0003_signal_commands.js.map +1 -0
  329. package/dist/esm/migrations/0004_approval_decisions.d.ts +13 -0
  330. package/dist/esm/migrations/0004_approval_decisions.d.ts.map +1 -0
  331. package/dist/esm/migrations/0004_approval_decisions.js +25 -0
  332. package/dist/esm/migrations/0004_approval_decisions.js.map +1 -0
  333. package/dist/esm/migrations/0005_signal_principals.d.ts +18 -0
  334. package/dist/esm/migrations/0005_signal_principals.d.ts.map +1 -0
  335. package/dist/esm/migrations/0005_signal_principals.js +27 -0
  336. package/dist/esm/migrations/0005_signal_principals.js.map +1 -0
  337. package/dist/esm/migrations/0006_run_principals.d.ts +18 -0
  338. package/dist/esm/migrations/0006_run_principals.d.ts.map +1 -0
  339. package/dist/esm/migrations/0006_run_principals.js +30 -0
  340. package/dist/esm/migrations/0006_run_principals.js.map +1 -0
  341. package/dist/esm/migrations/0007_resume_consent.d.ts +18 -0
  342. package/dist/esm/migrations/0007_resume_consent.d.ts.map +1 -0
  343. package/dist/esm/migrations/0007_resume_consent.js +27 -0
  344. package/dist/esm/migrations/0007_resume_consent.js.map +1 -0
  345. package/dist/esm/test/TestControl.d.ts +19 -0
  346. package/dist/esm/test/TestControl.d.ts.map +1 -0
  347. package/dist/esm/test/TestControl.js +30 -0
  348. package/dist/esm/test/TestControl.js.map +1 -0
  349. package/docs/README.md +189 -0
  350. package/docs/api.md +982 -0
  351. package/docs/concepts/authority.md +109 -0
  352. package/docs/concepts/cancellation.md +129 -0
  353. package/docs/concepts/lineage.md +132 -0
  354. package/docs/concepts/ownership.md +139 -0
  355. package/docs/concepts/projections.md +203 -0
  356. package/docs/concepts/receipts.md +128 -0
  357. package/docs/guides/approvals.md +284 -0
  358. package/docs/guides/cancel-and-resume.md +162 -0
  359. package/docs/guides/durable-storage.md +147 -0
  360. package/docs/guides/implement-an-executor.md +173 -0
  361. package/docs/guides/ingest-a-webhook.md +177 -0
  362. package/docs/guides/list-runs.md +160 -0
  363. package/docs/guides/monitor-runs.md +176 -0
  364. package/docs/guides/observe-health.md +147 -0
  365. package/docs/guides/postgres-tests.md +5 -0
  366. package/docs/guides/serve-over-rpc.md +220 -0
  367. package/docs/guides/signal-a-run.md +53 -0
  368. package/docs/guides/steer-a-run.md +138 -0
  369. package/docs/guides/store-credentials.md +164 -0
  370. package/docs/guides/testing.md +139 -0
  371. package/docs/guides/watch-a-run.md +154 -0
  372. package/docs/installation.md +106 -0
  373. package/docs/quickstart.md +163 -0
  374. package/docs/troubleshooting.md +208 -0
  375. package/package.json +405 -3
  376. package/src/ApprovalAuthority.ts +114 -0
  377. package/src/Cancellation.ts +172 -0
  378. package/src/Channels.ts +493 -0
  379. package/src/Control.ts +337 -0
  380. package/src/ControlClient.ts +319 -0
  381. package/src/ControlError.ts +378 -0
  382. package/src/ControlExecutor.ts +490 -0
  383. package/src/ControlFacts.ts +383 -0
  384. package/src/ControlLive.ts +2113 -0
  385. package/src/ControlRpcs.ts +443 -0
  386. package/src/ControlRuntime.ts +1597 -0
  387. package/src/ControlSchema.ts +1380 -0
  388. package/src/ControlServer.ts +182 -0
  389. package/src/Credential.ts +310 -0
  390. package/src/CredentialCipher.ts +110 -0
  391. package/src/CredentialStore.ts +152 -0
  392. package/src/DispatchReader.ts +122 -0
  393. package/src/Health.ts +591 -0
  394. package/src/JevSessionChecker.ts +127 -0
  395. package/src/Lineage.ts +203 -0
  396. package/src/Migrations.ts +56 -0
  397. package/src/Monitor.ts +600 -0
  398. package/src/ScopedToken.ts +306 -0
  399. package/src/SqlControlRuntime.ts +2476 -0
  400. package/src/SqlCredentialStore.ts +148 -0
  401. package/src/Steering.ts +96 -0
  402. package/src/SystemFlows.ts +225 -0
  403. package/src/WebCryptoCipher.ts +169 -0
  404. package/src/WebhookChannel.ts +166 -0
  405. package/src/index.ts +188 -0
  406. package/src/internal/MutationBoundary.ts +46 -0
  407. package/src/internal/activeFibers.ts +22 -0
  408. package/src/internal/issues.ts +40 -0
  409. package/src/internal/planning.ts +262 -0
  410. package/src/internal/sqlSchemaErrors.ts +37 -0
  411. package/src/migrations/0001_control_tables.ts +99 -0
  412. package/src/migrations/0002_run_keys.ts +23 -0
  413. package/src/migrations/0003_signal_commands.ts +27 -0
  414. package/src/migrations/0004_approval_decisions.ts +25 -0
  415. package/src/migrations/0005_signal_principals.ts +27 -0
  416. package/src/migrations/0006_run_principals.ts +31 -0
  417. package/src/migrations/0007_resume_consent.ts +28 -0
  418. package/src/test/TestControl.ts +47 -0
package/src/Monitor.ts ADDED
@@ -0,0 +1,600 @@
1
+ /**
2
+ * Run health over the control plane: what a run's state means, and what to do
3
+ * about it.
4
+ *
5
+ * A control plane answers "what is this run doing". A monitor answers the
6
+ * question after it — "is that all right, and if not, what now" — and the two
7
+ * are deliberately separate modules. {@link classify} is a pure function of
8
+ * one observation, so the vocabulary an operator reads on a dashboard is the
9
+ * same one a heal loop branches on and the same one a test can enumerate.
10
+ * {@link run} is the loop that beats over `Control` and journals what it saw.
11
+ *
12
+ * The classification is decided from durable evidence only: the run summary
13
+ * the control plane projects, and the journal entries `watch` replays. Nothing
14
+ * here reads an in-process fiber, which is what makes a monitor able to watch
15
+ * a run in another process at all.
16
+ *
17
+ * @since 0.1.0
18
+ */
19
+
20
+ import { Journal, JournalEvent } from "@smthrs/journal"
21
+ import { Duration, Effect, Stream } from "effect"
22
+ import { Control } from "./Control.ts"
23
+ import type { ControlError } from "./ControlError.ts"
24
+ import { PersistenceError } from "./ControlError.ts"
25
+ import type { ControlEvent, Receipt, RunId, RunSummary, WatchFilter } from "./ControlSchema.ts"
26
+ import * as SubjectHealth from "./Health.ts"
27
+
28
+ /**
29
+ * The journal event type the engine records when an action attempt starts.
30
+ *
31
+ * Named as a string rather than imported, exactly as in `Lineage`: a monitor
32
+ * reads journals, not engines.
33
+ *
34
+ * @category constants
35
+ * @since 0.1.0
36
+ */
37
+ export const attemptStartedEventType = "flows.engine.attempt-started"
38
+
39
+ /**
40
+ * The journal event type the engine records when an action attempt settles.
41
+ *
42
+ * @category constants
43
+ * @since 0.1.0
44
+ */
45
+ export const attemptFinishedEventType = "flows.engine.attempt-finished"
46
+
47
+ /**
48
+ * The journal event type one monitor beat is recorded under.
49
+ *
50
+ * @category constants
51
+ * @since 0.1.0
52
+ */
53
+ export const beatEventType = "control.monitor.beat"
54
+
55
+ /**
56
+ * The journal event type one applied remedy is recorded under.
57
+ *
58
+ * A beat and a heal are two records because they are two facts, and only one
59
+ * of them is true before the remedy runs. A single record carrying `healed`
60
+ * has to be written either before the remedy — leaving durable evidence of a
61
+ * heal that a crash one instruction later never performed — or after it, which
62
+ * loses the evidence of what a monitor decided when the remedy is what killed
63
+ * it. Splitting them keeps both.
64
+ *
65
+ * @category constants
66
+ * @since 0.1.0
67
+ */
68
+ export const healedEventType = "control.monitor.healed"
69
+
70
+ /**
71
+ * What a run looks like to a monitor.
72
+ *
73
+ * `healthy` covers both "moving" and "finished": a completed run needs nothing
74
+ * done to it, and neither does one that is making progress. The other six name
75
+ * a specific thing that is wrong, because a heal that cannot tell them apart
76
+ * would resume a run that is waiting for a human.
77
+ *
78
+ * @category models
79
+ * @since 0.1.0
80
+ */
81
+ export const Health = SubjectHealth.HealthState
82
+
83
+ /**
84
+ * What a run looks like to a monitor.
85
+ *
86
+ * @category models
87
+ * @since 0.1.0
88
+ */
89
+ export type Health = typeof Health.Type
90
+
91
+ /**
92
+ * Everything one classification is decided from.
93
+ *
94
+ * `events` is the run's journal as `watch` projects it, oldest first.
95
+ * `beatsWithoutProgress` counts consecutive beats that added no entry, and
96
+ * `stallBeats` is how many of those make a stall. Splitting the count from the
97
+ * threshold is what lets the same pure function serve a monitor that beats
98
+ * every second and one that beats every hour.
99
+ *
100
+ * @category models
101
+ * @since 0.1.0
102
+ */
103
+ export interface Observation {
104
+ /** The run's projection, or nothing when the control plane has no such run. */
105
+ readonly summary?: RunSummary | undefined
106
+ readonly events: ReadonlyArray<ControlEvent>
107
+ readonly beatsWithoutProgress: number
108
+ readonly stallBeats: number
109
+ /**
110
+ * The trampoline round at which a lineage stops being a loop and starts
111
+ * being a runaway. A run past it is looping without converging.
112
+ */
113
+ readonly roundBound?: number | undefined
114
+ /** Fresh, explicitly reported semantic work; never inferred from bytes or monitor events. */
115
+ readonly semanticProgress?: boolean | undefined
116
+ }
117
+
118
+ /** How many rounds a trampoline may take before a monitor calls it runaway. */
119
+ const defaultRoundBound = 32
120
+
121
+ interface AttemptState {
122
+ open: number
123
+ failed: boolean
124
+ }
125
+
126
+ const foldAttempt = (state: AttemptState, event: ControlEvent): void => {
127
+ if (event.kind === attemptStartedEventType) state.open += 1
128
+ if (event.kind === attemptFinishedEventType) {
129
+ state.open -= 1
130
+ const payload = event.payload
131
+ state.failed = typeof payload === "object" && payload !== null && !Array.isArray(payload) &&
132
+ (payload as { readonly state?: unknown })["state"] === "failed"
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Decides what a run's state means.
138
+ *
139
+ * The order is the order an operator would read it in, and each rule earns its
140
+ * place by naming a different response:
141
+ *
142
+ * | Condition | Health | Because |
143
+ * | --- | --- | --- |
144
+ * | No summary | `unknown` | Nothing to say, and nothing to do. |
145
+ * | `failed` | `failing` | The run itself reported the failure. |
146
+ * | `completed`, `cancelled` | `healthy` | A finished run needs nothing. |
147
+ * | `waiting-approval`, or parked on `approval` | `awaiting-human` | A human owes it an answer; no machine can supply one. |
148
+ * | Parked with no waiting reason | `awaiting-human` | Only an operator's own park writes no waiting reason, so a person stopped it and a person restarts it. |
149
+ * | `roundOrdinal` at or past the bound | `runaway-loop` | The lineage is looping without converging. |
150
+ * | The last settled attempt failed | `failing` | The run is still alive but its work is not landing. |
151
+ * | No progress for `stallBeats`, an attempt open | `wedged-node` | One attempt started and never settled: the work is stuck, not the run. |
152
+ * | No progress for `stallBeats` | `stalled` | Nothing is happening and nothing is in flight. |
153
+ * | Anything else | `healthy` | Entries are still arriving. |
154
+ *
155
+ * `awaiting-human` deliberately outranks `failing`: a run parked for approval
156
+ * after a failed attempt is waiting for a person, and resuming or cancelling
157
+ * it would take the decision away from them.
158
+ *
159
+ * A park with no reason is the same case. The engine names every park it makes
160
+ * — `event`, `approval`, `timer`, `quota`, `released` — so a parked run whose
161
+ * `waitingReason` is absent was parked by an operator through
162
+ * `ControlRuntime.writeStatus`. Calling that a stall and resuming it would undo
163
+ * a deliberate act, which is the worst thing an unattended heal loop can do.
164
+ *
165
+ * @param observation what the beat saw
166
+ * @category projections
167
+ * @since 0.1.0
168
+ */
169
+ export const classify = (observation: Observation): Health => {
170
+ const attempts: AttemptState = { open: 0, failed: false }
171
+ for (const event of observation.events) foldAttempt(attempts, event)
172
+ return classifyState(observation, attempts)
173
+ }
174
+
175
+ const classifyState = (observation: Omit<Observation, "events">, attempts: AttemptState): Health => {
176
+ const summary = observation.summary
177
+ if (summary === undefined) return "unknown"
178
+ if (summary.status === "failed") return "failing"
179
+ if (summary.status === "completed" || summary.status === "cancelled") return "healthy"
180
+ if (summary.status === "waiting-approval") return "awaiting-human"
181
+ if (summary.status === "parked" && (summary.waitingReason === "approval" || summary.waitingReason === undefined)) {
182
+ return "awaiting-human"
183
+ }
184
+ if (SubjectHealth.waitReason(summary.status, summary.waitingReason) !== undefined) return "healthy"
185
+ const bound = observation.roundBound ?? defaultRoundBound
186
+ if (summary.roundOrdinal !== undefined && summary.roundOrdinal >= bound) return "runaway-loop"
187
+ if (attempts.failed) return "failing"
188
+ if (!observation.semanticProgress && observation.beatsWithoutProgress >= observation.stallBeats) {
189
+ return attempts.open > 0 ? "wedged-node" : "stalled"
190
+ }
191
+ return "healthy"
192
+ }
193
+
194
+ /**
195
+ * What a monitor does about one unhealthy run.
196
+ *
197
+ * @category models
198
+ * @since 0.1.0
199
+ */
200
+ export type Remedy = "resume" | "cancel" | "none"
201
+
202
+ /**
203
+ * The remedy a health warrants, before `autoHeal` decides whether to apply it.
204
+ *
205
+ * A stalled or wedged run is one nobody is driving, and a resume claims it. A
206
+ * failing or runaway run is one that will not get better by being driven
207
+ * harder. Everything else is left alone.
208
+ *
209
+ * @param health what the beat classified
210
+ * @category projections
211
+ * @since 0.1.0
212
+ */
213
+ export const remedyFor = (health: Health): Remedy => {
214
+ switch (health) {
215
+ case "stalled":
216
+ case "wedged-node":
217
+ return "resume"
218
+ case "failing":
219
+ case "runaway-loop":
220
+ return "cancel"
221
+ default:
222
+ return "none"
223
+ }
224
+ }
225
+
226
+ /**
227
+ * One beat of a monitor.
228
+ *
229
+ * @category models
230
+ * @since 0.1.0
231
+ */
232
+ export interface Beat {
233
+ /** Which beat this was, counting from zero. */
234
+ readonly beat: number
235
+ readonly health: Health
236
+ /** The newest journal sequence the run had, ignoring the monitor's own records. */
237
+ readonly sequence: number
238
+ /**
239
+ * The remedy that ran and returned, absent when none did.
240
+ *
241
+ * Present only after the heal succeeded, which is also when
242
+ * `control.monitor.healed` is journaled. A beat that decided on a remedy the
243
+ * monitor never got to apply reports nothing here.
244
+ */
245
+ readonly healed?: Remedy | undefined
246
+ /** The receipt the remedy returned, absent when none was applied. */
247
+ readonly receipt?: Receipt | undefined
248
+ }
249
+
250
+ /**
251
+ * What a monitor found.
252
+ *
253
+ * @category models
254
+ * @since 0.1.0
255
+ */
256
+ export interface Report {
257
+ readonly runId: RunId
258
+ readonly beats: ReadonlyArray<Beat>
259
+ /** The last beat's health, or `unknown` when no beat ran. */
260
+ readonly health: Health
261
+ }
262
+
263
+ /**
264
+ * How a monitor beats.
265
+ *
266
+ * @category models
267
+ * @since 0.1.0
268
+ */
269
+ export interface Options {
270
+ readonly runId: RunId
271
+ /** Optional configured observational checker; checker results never authorize a remedy. */
272
+ readonly healthCheck?: SubjectHealth.ResolvedCheck | undefined
273
+ /** Shared host admission limit around probes. */
274
+ readonly withProbePermit?: (<A>(effect: Effect.Effect<A>) => Effect.Effect<A>) | undefined
275
+ /** Bounded retained beat history for long-lived host monitoring. */
276
+ readonly retainBeats?: number | undefined
277
+ /** Hosts with structured status observations omit redundant legacy heartbeat records. */
278
+ readonly recordBeats?: boolean | undefined
279
+ /**
280
+ * Who is watching. Defaults to `default`.
281
+ *
282
+ * It reaches the journal twice: as the source of every record this monitor
283
+ * writes, and as `monitorId` in each payload. Two monitors pointed at one
284
+ * run both beat and both remedy — nothing on the control plane leases a run
285
+ * to one watcher — so the identity is what makes their evidence tellable
286
+ * apart, and what keeps their remedies on separate idempotency keys.
287
+ */
288
+ readonly monitorId?: string | undefined
289
+ /** How long to wait between beats. Defaults to one second. */
290
+ readonly intervalMs?: number | undefined
291
+ /** How many beats to take before reporting. Defaults to ten. */
292
+ readonly maxChecks?: number | undefined
293
+ /** How many beats without progress make a stall. Defaults to three. */
294
+ readonly stallBeats?: number | undefined
295
+ /** The round at which a trampoline is a runaway. Defaults to 32. */
296
+ readonly roundBound?: number | undefined
297
+ /** Which healths the monitor is allowed to act on. Defaults to none. */
298
+ readonly autoHeal?: ReadonlyArray<Health> | undefined
299
+ /**
300
+ * What to do about an unhealthy run. Defaults to `Control.resume` and
301
+ * `Control.cancel` per {@link remedyFor}.
302
+ *
303
+ * A heal that fails does not abort the monitor: the failure is logged, the
304
+ * beat is recorded without `healed` or `receipt`, and the loop moves to the
305
+ * next beat, where the stall evidence still stands and the remedy is
306
+ * retried under that beat's own idempotency key.
307
+ */
308
+ readonly heal?: (
309
+ input: { readonly runId: RunId; readonly health: Health; readonly remedy: Remedy; readonly beat: number }
310
+ ) => Effect.Effect<Receipt, ControlError>
311
+ }
312
+
313
+ const summaryOf = (runId: RunId): Effect.Effect<RunSummary | undefined, ControlError, Control> =>
314
+ Effect.flatMap(Control, (control) =>
315
+ control.list({ _tag: "runs", filters: { runId } }).pipe(
316
+ Effect.map((listed) => listed._tag === "runs" ? listed.items[0] : undefined)
317
+ ))
318
+
319
+ const terminal = (summary: RunSummary | undefined): boolean =>
320
+ summary !== undefined &&
321
+ (summary.status === "completed" || summary.status === "failed" || summary.status === "cancelled")
322
+
323
+ /**
324
+ * Watches one run and, when told to, heals it.
325
+ *
326
+ * The loop stops early on a terminal run, because a finished run has nothing
327
+ * left to observe and beating at it would fill its journal with heartbeats.
328
+ * Otherwise it takes `maxChecks` beats and reports what it saw.
329
+ *
330
+ * Every beat is journaled as `control.monitor.beat` before any remedy is
331
+ * applied, carrying the `remedy` it is about to attempt, so a monitor that
332
+ * crashes mid-heal leaves the evidence of what it decided. The remedy itself
333
+ * is journaled separately as `control.monitor.healed`, and only once the heal
334
+ * returned a receipt: a heal that failed, or a process that died running one,
335
+ * must not leave a durable record saying the run was healed.
336
+ *
337
+ * `autoHeal` is empty by default. A monitor that healed by default would be a
338
+ * monitor that cancels a run the first time it looks at one, which is the
339
+ * wrong default for a thing an operator points at production.
340
+ *
341
+ * Nothing here leases the run. Two monitors on one run both beat and both
342
+ * remedy, so a remedy has to be idempotent on the control plane — which the
343
+ * two defaults are, through `Control.resume` and `Control.cancel`
344
+ * idempotency keys that name the monitor, the run, and the beat. A custom
345
+ * `heal` owes the same property.
346
+ *
347
+ * @param options how to beat and what to heal
348
+ * @category constructors
349
+ * @since 0.1.0
350
+ */
351
+ export const run = (
352
+ options: Options
353
+ ): Effect.Effect<Report, ControlError, Control | Journal.Journal> =>
354
+ Effect.gen(function*() {
355
+ const control = yield* Control
356
+ const journal = yield* Journal.Journal
357
+ const intervalMs = options.intervalMs ?? options.healthCheck?.policy.intervalMs ?? 1_000
358
+ let consecutiveProbeFailures = 0
359
+ let lastPublished: SubjectHealth.HealthObservation | undefined
360
+ const maxChecks = options.maxChecks ?? 10
361
+ const stallBeats = options.stallBeats ?? 3
362
+ const autoHeal = options.autoHeal ?? []
363
+ const monitorId = options.monitorId ?? "default"
364
+ const heal = options.heal ?? ((input) =>
365
+ input.remedy === "resume"
366
+ ? control.resume({
367
+ runId: input.runId,
368
+ idempotencyKey: `monitor:${monitorId}:resume:${input.runId}:${input.beat}`
369
+ })
370
+ : control.cancel({
371
+ runId: input.runId,
372
+ reason: `monitor:${input.health}`,
373
+ idempotencyKey: `monitor:${monitorId}:cancel:${input.runId}:${input.beat}`
374
+ }))
375
+
376
+ const emit = (
377
+ eventType: string,
378
+ payload: Record<string, unknown>,
379
+ operation: string
380
+ ): Effect.Effect<void, ControlError> => {
381
+ const unrecorded = (cause: unknown) =>
382
+ new PersistenceError({
383
+ operation: eventType,
384
+ message: `Failed to record ${operation} for ${options.runId}`,
385
+ cause
386
+ })
387
+ // The record is BUILT inside the failure channel, not before it.
388
+ // `JournalEvent.RunId`/`SourceId` refuse an identifier the store cannot
389
+ // tell apart from another one — a lone UTF-16 surrogate, an embedded NUL
390
+ // — by THROWING, and `monitorId` reaches here from an operator's flag. A
391
+ // thrown constructor is a defect, which the RPC boundary reports as an
392
+ // opaque `TransportError` and a caller cannot handle; a beat whose record
393
+ // cannot be built is the same event as a beat whose record cannot be
394
+ // appended, so both fail as `PersistenceError` naming the record.
395
+ return Effect.try({
396
+ try: () =>
397
+ new JournalEvent.Input({
398
+ runId: JournalEvent.RunId.make(options.runId),
399
+ sourceId: JournalEvent.SourceId.make(`/control/monitor/${monitorId}`),
400
+ eventType,
401
+ payload: { runId: options.runId, monitorId, ...payload }
402
+ }),
403
+ catch: unrecorded
404
+ }).pipe(
405
+ Effect.flatMap((input) => Effect.mapError(journal.emitDurableUnfenced(input), unrecorded)),
406
+ Effect.asVoid
407
+ )
408
+ }
409
+
410
+ /** What the beat saw, and what it is about to do about it. */
411
+ const record = (beat: Beat, remedy: Remedy): Effect.Effect<void, ControlError> =>
412
+ emit(
413
+ beatEventType,
414
+ {
415
+ beat: beat.beat,
416
+ health: beat.health,
417
+ sequence: beat.sequence,
418
+ ...(remedy === "none" ? {} : { remedy })
419
+ },
420
+ `monitor beat ${beat.beat}`
421
+ )
422
+
423
+ /** What the remedy did, once it returned. */
424
+ const recordHealed = (
425
+ beat: Beat,
426
+ remedy: Remedy,
427
+ receipt: Receipt
428
+ ): Effect.Effect<void, ControlError> =>
429
+ emit(
430
+ healedEventType,
431
+ { beat: beat.beat, health: beat.health, healed: remedy, receipt: receipt._tag },
432
+ `monitor heal ${beat.beat}`
433
+ )
434
+
435
+ const beats: Array<Beat> = []
436
+ let beatsWithoutProgress = 0
437
+ let lastSequence = -1
438
+ let checkpoint: Pick<WatchFilter, "afterCursor" | "afterSequence"> = {}
439
+ const attempts: AttemptState = { open: 0, failed: false }
440
+ let sequence = -1
441
+ for (let beat = 0; beat < maxChecks; beat += 1) {
442
+ if (beat > 0 && intervalMs > 0) {
443
+ yield* Effect.sleep(Duration.millis(
444
+ options.healthCheck === undefined
445
+ ? intervalMs
446
+ : SubjectHealth.nextDelay(options.healthCheck.policy, consecutiveProbeFailures)
447
+ ))
448
+ }
449
+ const summary = yield* summaryOf(options.runId)
450
+ const newEvents: Array<ControlEvent> = []
451
+ const sinceCursor = Math.max(0, sequence)
452
+ yield* control.watch({ runId: options.runId, follow: false, ...checkpoint }).pipe(
453
+ Stream.runForEach((event) =>
454
+ Effect.sync(() => {
455
+ // Advance over bookkeeping too, but never count it as run progress.
456
+ checkpoint = event.cursor === undefined
457
+ ? { afterSequence: event.sequence }
458
+ : { afterCursor: event.cursor }
459
+ if (isBookkeepingEvent(event.kind)) return
460
+ newEvents.push(event)
461
+ if (newEvents.length > 256) newEvents.shift()
462
+ sequence = event.sequence
463
+ foldAttempt(attempts, event)
464
+ })
465
+ )
466
+ )
467
+ beatsWithoutProgress = sequence === lastSequence ? beatsWithoutProgress + 1 : 0
468
+ lastSequence = sequence
469
+ const health = classifyState({
470
+ ...(summary === undefined ? {} : { summary }),
471
+ beatsWithoutProgress,
472
+ stallBeats,
473
+ ...(options.roundBound === undefined ? {} : { roundBound: options.roundBound })
474
+ }, attempts)
475
+ if (options.healthCheck !== undefined && summary !== undefined) {
476
+ const check = options.healthCheck
477
+ const subjectId = `run:${options.runId}`
478
+ const incarnation = SubjectHealth.runIncarnation(summary)
479
+ const probe = SubjectHealth.evaluate(check, {
480
+ subjectId,
481
+ state: summary.status,
482
+ summary,
483
+ events: newEvents,
484
+ sinceCursor
485
+ }, { monitorId, incarnation, evidenceSeq: Math.max(0, sequence) })
486
+ let observation = yield* (options.withProbePermit?.(probe) ?? probe)
487
+ const current = yield* summaryOf(options.runId)
488
+ if (current === undefined || SubjectHealth.runIncarnation(current) !== incarnation) {
489
+ observation = { ...observation, outcome: "discarded", report: undefined, reason: "owner-changed" }
490
+ }
491
+ consecutiveProbeFailures = observation.outcome === "ok" ? 0 : consecutiveProbeFailures + 1
492
+ const semanticProgress = observation.outcome === "ok" && observation.report?.activity === "working"
493
+ const baseHealth = classifyState({
494
+ summary,
495
+ beatsWithoutProgress,
496
+ stallBeats,
497
+ roundBound: options.roundBound,
498
+ semanticProgress
499
+ }, attempts)
500
+ observation = { ...observation, baseHealth }
501
+ const status = SubjectHealth.rollup({
502
+ subjectId,
503
+ state: summary.status,
504
+ incarnation,
505
+ waitingReason: summary.waitingReason,
506
+ baseHealth,
507
+ latest: { observation, sequence: 0 },
508
+ now: observation.observedAt,
509
+ updatedAt: summary.updatedAt
510
+ })
511
+ const changed = lastPublished === undefined || lastPublished.incarnation !== observation.incarnation ||
512
+ lastPublished.outcome !== observation.outcome || lastPublished.baseHealth !== observation.baseHealth ||
513
+ lastPublished.evidenceSeq !== observation.evidenceSeq ||
514
+ JSON.stringify(lastPublished.report) !== JSON.stringify(observation.report) ||
515
+ lastPublished.reason !== observation.reason
516
+ // Renew before expiry, but do not fill the journal with identical per-probe records.
517
+ if (changed || observation.observedAt >= lastPublished!.observedAt + check.policy.ttlMs / 2) {
518
+ yield* emit(SubjectHealth.statusObservedEventType, {
519
+ ...observation,
520
+ status: summary.status,
521
+ health: status.health,
522
+ activity: status.activity,
523
+ attention: status.attention,
524
+ freshness: status.freshness,
525
+ waitingReason: summary.waitingReason ?? ""
526
+ }, "health observation")
527
+ lastPublished = observation
528
+ }
529
+ }
530
+ // Checker output is observational and never participates in remedy authorization.
531
+ const remedy = autoHeal.includes(health) ? remedyFor(health) : "none"
532
+ const observed: Beat = { beat, health, sequence }
533
+ if (options.recordBeats !== false) yield* record(observed, remedy)
534
+ if (remedy === "none") {
535
+ beats.push(observed)
536
+ } else {
537
+ const receipt = yield* heal({ runId: options.runId, health, remedy, beat }).pipe(
538
+ // A remedy that fails must not abort the run of beats: the monitor
539
+ // is an unattended loop, and one failing heal leaves every other
540
+ // beat — and every other run it would have observed — unwatched.
541
+ // The failure is logged with the beat it failed on, and the beat is
542
+ // recorded without `healed` or `receipt`, exactly as a remedy the
543
+ // monitor never got to apply reports.
544
+ Effect.catch((failure) =>
545
+ Effect.annotateLogs(
546
+ Effect.logWarning("A monitor remedy failed and was skipped"),
547
+ { runId: options.runId, beat, health, remedy, cause: String(failure) }
548
+ ).pipe(Effect.as(undefined))
549
+ )
550
+ )
551
+ if (receipt === undefined) {
552
+ // The stall evidence stands: nothing provably moved the run, so the
553
+ // next beat compares against the same progress mark and the remedy
554
+ // is retried with the beat's own idempotency key.
555
+ beats.push(observed)
556
+ } else {
557
+ // A remedy that returned is not a remedy that was applied. `Terminal`
558
+ // says the run had already settled, so nothing was healed; `Conflict`
559
+ // says the key belonged to another mutation, so this monitor's remedy
560
+ // never ran. Recording either as `healed` claimed something that did
561
+ // not happen, and resetting the stall count on either erased the
562
+ // evidence the next beat needs to notice the run is still stuck.
563
+ const applied = receipt._tag === "Accepted" || receipt._tag === "AlreadyApplied"
564
+ if (applied) {
565
+ // Journaled here and not a line earlier: `healed` is a claim about
566
+ // something that happened, and until the receipt came back it had not.
567
+ yield* recordHealed(observed, remedy, receipt)
568
+ // The heal moved the run, so the next beat compares against a run
569
+ // that has changed. Counting the beats before it as stall evidence
570
+ // again would heal a second time for the same stall.
571
+ beatsWithoutProgress = 0
572
+ }
573
+ beats.push(applied ? { ...observed, healed: remedy, receipt } : { ...observed, receipt })
574
+ // A remedy that answered `Terminal` observed the run settle. The loop
575
+ // ends on the same evidence a terminal summary ends it on, rather than
576
+ // beating against a run nothing can move.
577
+ if (receipt._tag === "Terminal") break
578
+ }
579
+ }
580
+ if (options.retainBeats !== undefined && beats.length > options.retainBeats) {
581
+ beats.splice(0, beats.length - options.retainBeats)
582
+ }
583
+ if (terminal(summary)) {
584
+ break
585
+ }
586
+ }
587
+ return {
588
+ runId: options.runId,
589
+ beats,
590
+ health: beats[beats.length - 1]?.health ?? "unknown"
591
+ }
592
+ })
593
+
594
+ /** Observer and notification bookkeeping is never execution progress.
595
+ * @category predicates
596
+ * @since 1.0.0
597
+ */
598
+ export const isBookkeepingEvent = (kind: string): boolean =>
599
+ kind.startsWith("control.monitor.") || kind.startsWith("control.status.") ||
600
+ kind.startsWith("flows.alerts.") || kind.startsWith("flows.notifications.") || kind.startsWith("flows.notification.")