@smthrs/platform-node 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 (442) hide show
  1. package/CHANGELOG.md +211 -0
  2. package/LICENSE +21 -0
  3. package/README.md +165 -2
  4. package/bin/darwin-arm64/smithers-jj-export +0 -0
  5. package/bin/darwin-x64/smithers-jj-export +0 -0
  6. package/bin/linux-arm64/smithers-jj-export +0 -0
  7. package/bin/linux-x64/smithers-jj-export +4 -0
  8. package/bin/win32-x64/smithers-jj-export.exe +0 -0
  9. package/dist/cjs/AtomicFileSystem.d.ts +147 -0
  10. package/dist/cjs/AtomicFileSystem.d.ts.map +1 -0
  11. package/dist/cjs/AtomicFileSystem.js +234 -0
  12. package/dist/cjs/AtomicFileSystem.js.map +7 -0
  13. package/dist/cjs/EgressHttpClient.d.ts +106 -0
  14. package/dist/cjs/EgressHttpClient.d.ts.map +1 -0
  15. package/dist/cjs/EgressHttpClient.js +202 -0
  16. package/dist/cjs/EgressHttpClient.js.map +7 -0
  17. package/dist/cjs/HostLiveness.d.ts +81 -0
  18. package/dist/cjs/HostLiveness.d.ts.map +1 -0
  19. package/dist/cjs/HostLiveness.js +49 -0
  20. package/dist/cjs/HostLiveness.js.map +7 -0
  21. package/dist/cjs/NodeHost.d.ts +145 -0
  22. package/dist/cjs/NodeHost.d.ts.map +1 -0
  23. package/dist/cjs/NodeHost.js +128 -0
  24. package/dist/cjs/NodeHost.js.map +7 -0
  25. package/dist/cjs/ProcessConfinement.d.ts +61 -0
  26. package/dist/cjs/ProcessConfinement.d.ts.map +1 -0
  27. package/dist/cjs/ProcessConfinement.js +287 -0
  28. package/dist/cjs/ProcessConfinement.js.map +7 -0
  29. package/dist/cjs/ProcessReaper.d.ts +457 -0
  30. package/dist/cjs/ProcessReaper.d.ts.map +1 -0
  31. package/dist/cjs/ProcessReaper.js +349 -0
  32. package/dist/cjs/ProcessReaper.js.map +7 -0
  33. package/dist/cjs/ProcessSandbox.d.ts +377 -0
  34. package/dist/cjs/ProcessSandbox.d.ts.map +1 -0
  35. package/dist/cjs/ProcessSandbox.js +759 -0
  36. package/dist/cjs/ProcessSandbox.js.map +7 -0
  37. package/dist/cjs/ScopedProcess.d.ts +65 -0
  38. package/dist/cjs/ScopedProcess.d.ts.map +1 -0
  39. package/dist/cjs/ScopedProcess.js +87 -0
  40. package/dist/cjs/ScopedProcess.js.map +7 -0
  41. package/dist/cjs/index.d.ts +38 -0
  42. package/dist/cjs/index.d.ts.map +1 -0
  43. package/dist/cjs/index.js +47 -0
  44. package/dist/cjs/index.js.map +7 -0
  45. package/dist/cjs/internal/AtomicFileSystemExecutable.d.ts +61 -0
  46. package/dist/cjs/internal/AtomicFileSystemExecutable.d.ts.map +1 -0
  47. package/dist/cjs/internal/AtomicFileSystemExecutable.js +250 -0
  48. package/dist/cjs/internal/AtomicFileSystemExecutable.js.map +7 -0
  49. package/dist/cjs/internal/AtomicFileSystemProtocol.d.ts +78 -0
  50. package/dist/cjs/internal/AtomicFileSystemProtocol.d.ts.map +1 -0
  51. package/dist/cjs/internal/AtomicFileSystemProtocol.js +382 -0
  52. package/dist/cjs/internal/AtomicFileSystemProtocol.js.map +7 -0
  53. package/dist/cjs/internal/AtomicFileSystemTransport.d.ts +44 -0
  54. package/dist/cjs/internal/AtomicFileSystemTransport.d.ts.map +1 -0
  55. package/dist/cjs/internal/AtomicFileSystemTransport.js +197 -0
  56. package/dist/cjs/internal/AtomicFileSystemTransport.js.map +7 -0
  57. package/dist/cjs/internal/AtomicHelperError.d.ts +35 -0
  58. package/dist/cjs/internal/AtomicHelperError.d.ts.map +1 -0
  59. package/dist/cjs/internal/AtomicHelperError.js +45 -0
  60. package/dist/cjs/internal/AtomicHelperError.js.map +7 -0
  61. package/dist/cjs/internal/AtomicHelperFault.d.ts +59 -0
  62. package/dist/cjs/internal/AtomicHelperFault.d.ts.map +1 -0
  63. package/dist/cjs/internal/AtomicHelperFault.js +66 -0
  64. package/dist/cjs/internal/AtomicHelperFault.js.map +7 -0
  65. package/dist/cjs/internal/PipedProcess.d.ts +35 -0
  66. package/dist/cjs/internal/PipedProcess.d.ts.map +1 -0
  67. package/dist/cjs/internal/PipedProcess.js +309 -0
  68. package/dist/cjs/internal/PipedProcess.js.map +7 -0
  69. package/dist/cjs/internal/ProcSnapshot.d.ts +85 -0
  70. package/dist/cjs/internal/ProcSnapshot.d.ts.map +1 -0
  71. package/dist/cjs/internal/ProcSnapshot.js +123 -0
  72. package/dist/cjs/internal/ProcSnapshot.js.map +7 -0
  73. package/dist/cjs/internal/ProcessCleanup.d.ts +64 -0
  74. package/dist/cjs/internal/ProcessCleanup.d.ts.map +1 -0
  75. package/dist/cjs/internal/ProcessCleanup.js +62 -0
  76. package/dist/cjs/internal/ProcessCleanup.js.map +7 -0
  77. package/dist/cjs/internal/ProcessCleanupPolicyError.d.ts +30 -0
  78. package/dist/cjs/internal/ProcessCleanupPolicyError.d.ts.map +1 -0
  79. package/dist/cjs/internal/ProcessCleanupPolicyError.js +33 -0
  80. package/dist/cjs/internal/ProcessCleanupPolicyError.js.map +7 -0
  81. package/dist/cjs/internal/ProcessFault.d.ts +63 -0
  82. package/dist/cjs/internal/ProcessFault.d.ts.map +1 -0
  83. package/dist/cjs/internal/ProcessFault.js +63 -0
  84. package/dist/cjs/internal/ProcessFault.js.map +7 -0
  85. package/dist/cjs/internal/ProcessSupervisor.d.ts +141 -0
  86. package/dist/cjs/internal/ProcessSupervisor.d.ts.map +1 -0
  87. package/dist/cjs/internal/ProcessSupervisor.js +682 -0
  88. package/dist/cjs/internal/ProcessSupervisor.js.map +7 -0
  89. package/dist/cjs/internal/SupervisorProgram.d.ts +9 -0
  90. package/dist/cjs/internal/SupervisorProgram.d.ts.map +1 -0
  91. package/dist/cjs/internal/SupervisorProgram.js +306 -0
  92. package/dist/cjs/internal/SupervisorProgram.js.map +7 -0
  93. package/dist/cjs/internal/WindowsProcessJob.d.ts +39 -0
  94. package/dist/cjs/internal/WindowsProcessJob.d.ts.map +1 -0
  95. package/dist/cjs/internal/WindowsProcessJob.js +113 -0
  96. package/dist/cjs/internal/WindowsProcessJob.js.map +7 -0
  97. package/dist/cjs/package.json +1 -0
  98. package/dist/esm/AtomicFileSystem.d.ts +147 -0
  99. package/dist/esm/AtomicFileSystem.d.ts.map +1 -0
  100. package/dist/esm/AtomicFileSystem.js +290 -0
  101. package/dist/esm/AtomicFileSystem.js.map +1 -0
  102. package/dist/esm/EgressHttpClient.d.ts +106 -0
  103. package/dist/esm/EgressHttpClient.d.ts.map +1 -0
  104. package/dist/esm/EgressHttpClient.js +259 -0
  105. package/dist/esm/EgressHttpClient.js.map +1 -0
  106. package/dist/esm/HostLiveness.d.ts +81 -0
  107. package/dist/esm/HostLiveness.d.ts.map +1 -0
  108. package/dist/esm/HostLiveness.js +68 -0
  109. package/dist/esm/HostLiveness.js.map +1 -0
  110. package/dist/esm/NodeHost.d.ts +145 -0
  111. package/dist/esm/NodeHost.d.ts.map +1 -0
  112. package/dist/esm/NodeHost.js +162 -0
  113. package/dist/esm/NodeHost.js.map +1 -0
  114. package/dist/esm/ProcessConfinement.d.ts +61 -0
  115. package/dist/esm/ProcessConfinement.d.ts.map +1 -0
  116. package/dist/esm/ProcessConfinement.js +311 -0
  117. package/dist/esm/ProcessConfinement.js.map +1 -0
  118. package/dist/esm/ProcessReaper.d.ts +457 -0
  119. package/dist/esm/ProcessReaper.d.ts.map +1 -0
  120. package/dist/esm/ProcessReaper.js +711 -0
  121. package/dist/esm/ProcessReaper.js.map +1 -0
  122. package/dist/esm/ProcessSandbox.d.ts +377 -0
  123. package/dist/esm/ProcessSandbox.d.ts.map +1 -0
  124. package/dist/esm/ProcessSandbox.js +1014 -0
  125. package/dist/esm/ProcessSandbox.js.map +1 -0
  126. package/dist/esm/ScopedProcess.d.ts +65 -0
  127. package/dist/esm/ScopedProcess.d.ts.map +1 -0
  128. package/dist/esm/ScopedProcess.js +66 -0
  129. package/dist/esm/ScopedProcess.js.map +1 -0
  130. package/dist/esm/index.d.ts +38 -0
  131. package/dist/esm/index.d.ts.map +1 -0
  132. package/dist/esm/index.js +38 -0
  133. package/dist/esm/index.js.map +1 -0
  134. package/dist/esm/internal/AtomicFileSystemExecutable.d.ts +61 -0
  135. package/dist/esm/internal/AtomicFileSystemExecutable.d.ts.map +1 -0
  136. package/dist/esm/internal/AtomicFileSystemExecutable.js +350 -0
  137. package/dist/esm/internal/AtomicFileSystemExecutable.js.map +1 -0
  138. package/dist/esm/internal/AtomicFileSystemProtocol.d.ts +78 -0
  139. package/dist/esm/internal/AtomicFileSystemProtocol.d.ts.map +1 -0
  140. package/dist/esm/internal/AtomicFileSystemProtocol.js +412 -0
  141. package/dist/esm/internal/AtomicFileSystemProtocol.js.map +1 -0
  142. package/dist/esm/internal/AtomicFileSystemTransport.d.ts +44 -0
  143. package/dist/esm/internal/AtomicFileSystemTransport.d.ts.map +1 -0
  144. package/dist/esm/internal/AtomicFileSystemTransport.js +242 -0
  145. package/dist/esm/internal/AtomicFileSystemTransport.js.map +1 -0
  146. package/dist/esm/internal/AtomicHelperError.d.ts +35 -0
  147. package/dist/esm/internal/AtomicHelperError.d.ts.map +1 -0
  148. package/dist/esm/internal/AtomicHelperError.js +39 -0
  149. package/dist/esm/internal/AtomicHelperError.js.map +1 -0
  150. package/dist/esm/internal/AtomicHelperFault.d.ts +59 -0
  151. package/dist/esm/internal/AtomicHelperFault.d.ts.map +1 -0
  152. package/dist/esm/internal/AtomicHelperFault.js +66 -0
  153. package/dist/esm/internal/AtomicHelperFault.js.map +1 -0
  154. package/dist/esm/internal/PipedProcess.d.ts +35 -0
  155. package/dist/esm/internal/PipedProcess.d.ts.map +1 -0
  156. package/dist/esm/internal/PipedProcess.js +321 -0
  157. package/dist/esm/internal/PipedProcess.js.map +1 -0
  158. package/dist/esm/internal/ProcSnapshot.d.ts +85 -0
  159. package/dist/esm/internal/ProcSnapshot.d.ts.map +1 -0
  160. package/dist/esm/internal/ProcSnapshot.js +188 -0
  161. package/dist/esm/internal/ProcSnapshot.js.map +1 -0
  162. package/dist/esm/internal/ProcessCleanup.d.ts +64 -0
  163. package/dist/esm/internal/ProcessCleanup.d.ts.map +1 -0
  164. package/dist/esm/internal/ProcessCleanup.js +45 -0
  165. package/dist/esm/internal/ProcessCleanup.js.map +1 -0
  166. package/dist/esm/internal/ProcessCleanupPolicyError.d.ts +30 -0
  167. package/dist/esm/internal/ProcessCleanupPolicyError.d.ts.map +1 -0
  168. package/dist/esm/internal/ProcessCleanupPolicyError.js +25 -0
  169. package/dist/esm/internal/ProcessCleanupPolicyError.js.map +1 -0
  170. package/dist/esm/internal/ProcessFault.d.ts +63 -0
  171. package/dist/esm/internal/ProcessFault.d.ts.map +1 -0
  172. package/dist/esm/internal/ProcessFault.js +64 -0
  173. package/dist/esm/internal/ProcessFault.js.map +1 -0
  174. package/dist/esm/internal/ProcessSupervisor.d.ts +141 -0
  175. package/dist/esm/internal/ProcessSupervisor.d.ts.map +1 -0
  176. package/dist/esm/internal/ProcessSupervisor.js +682 -0
  177. package/dist/esm/internal/ProcessSupervisor.js.map +1 -0
  178. package/dist/esm/internal/SupervisorProgram.d.ts +9 -0
  179. package/dist/esm/internal/SupervisorProgram.d.ts.map +1 -0
  180. package/dist/esm/internal/SupervisorProgram.js +290 -0
  181. package/dist/esm/internal/SupervisorProgram.js.map +1 -0
  182. package/dist/esm/internal/WindowsProcessJob.d.ts +39 -0
  183. package/dist/esm/internal/WindowsProcessJob.d.ts.map +1 -0
  184. package/dist/esm/internal/WindowsProcessJob.js +115 -0
  185. package/dist/esm/internal/WindowsProcessJob.js.map +1 -0
  186. package/dist/vendor/adapters.json +1757 -0
  187. package/dist/vendor/chunk-2Y7WC4KU.js +109 -0
  188. package/dist/vendor/chunk-3GR5VZ2M.js +118 -0
  189. package/dist/vendor/chunk-42QKY6T6.js +40 -0
  190. package/dist/vendor/chunk-54RLBMTZ.js +61 -0
  191. package/dist/vendor/chunk-5ZMHBXIK.js +55 -0
  192. package/dist/vendor/chunk-6ME2W7TF.js +28 -0
  193. package/dist/vendor/chunk-AMQKOQNT.js +34 -0
  194. package/dist/vendor/chunk-B3FZ67QU.js +59 -0
  195. package/dist/vendor/chunk-BKTBUBQY.js +437 -0
  196. package/dist/vendor/chunk-DS7GF46S.js +34 -0
  197. package/dist/vendor/chunk-DVOAHXPG.js +104 -0
  198. package/dist/vendor/chunk-DWSHTSVX.js +229 -0
  199. package/dist/vendor/chunk-EGBNVEEW.js +14 -0
  200. package/dist/vendor/chunk-EZWY45HP.js +18 -0
  201. package/dist/vendor/chunk-FNZOJHSV.js +71 -0
  202. package/dist/vendor/chunk-GXCHI7BM.js +131 -0
  203. package/dist/vendor/chunk-IIJ52ZU6.js +20 -0
  204. package/dist/vendor/chunk-J7A4CZDO.js +92 -0
  205. package/dist/vendor/chunk-JG5TTWPT.js +85 -0
  206. package/dist/vendor/chunk-JJ24ARBQ.js +394 -0
  207. package/dist/vendor/chunk-KOZMM5PZ.js +426 -0
  208. package/dist/vendor/chunk-KUUXSID7.js +18 -0
  209. package/dist/vendor/chunk-LEUPYT3Q.js +36 -0
  210. package/dist/vendor/chunk-M32LDTW7.js +18 -0
  211. package/dist/vendor/chunk-M7CLUILA.js +0 -0
  212. package/dist/vendor/chunk-MBSKBQ5D.js +21 -0
  213. package/dist/vendor/chunk-MLKGABMK.js +9 -0
  214. package/dist/vendor/chunk-NBVRTDTV.js +129 -0
  215. package/dist/vendor/chunk-OXNGN5H7.js +81 -0
  216. package/dist/vendor/chunk-QPLDT6A6.js +564 -0
  217. package/dist/vendor/chunk-QYBKAIA6.js +18 -0
  218. package/dist/vendor/chunk-RO2R4MJH.js +62 -0
  219. package/dist/vendor/chunk-SIR62W3Y.js +398 -0
  220. package/dist/vendor/chunk-TFIKFFSC.js +26 -0
  221. package/dist/vendor/chunk-TXKLEBBL.js +143 -0
  222. package/dist/vendor/chunk-UFH5UUZK.js +48 -0
  223. package/dist/vendor/chunk-VANKHF46.js +96 -0
  224. package/dist/vendor/chunk-VZBDV3TV.js +18 -0
  225. package/dist/vendor/chunk-XDKNKFBW.js +22 -0
  226. package/dist/vendor/chunk-XH4U3JSV.js +249 -0
  227. package/dist/vendor/chunk-Y35ZAO5U.js +53 -0
  228. package/dist/vendor/chunk-ZJMRMY57.js +0 -0
  229. package/dist/vendor/cjs/node/NodeChildProcessSpawner.d.ts +7 -0
  230. package/dist/vendor/cjs/node/NodeChildProcessSpawner.js +1 -0
  231. package/dist/vendor/cjs/node/NodeClusterHttp.d.ts +63 -0
  232. package/dist/vendor/cjs/node/NodeClusterHttp.js +1 -0
  233. package/dist/vendor/cjs/node/NodeClusterSocket.d.ts +79 -0
  234. package/dist/vendor/cjs/node/NodeClusterSocket.js +1 -0
  235. package/dist/vendor/cjs/node/NodeCrypto.d.ts +10 -0
  236. package/dist/vendor/cjs/node/NodeCrypto.js +1 -0
  237. package/dist/vendor/cjs/node/NodeFileSystem.d.ts +10 -0
  238. package/dist/vendor/cjs/node/NodeFileSystem.js +1 -0
  239. package/dist/vendor/cjs/node/NodeHttpClient.d.ts +179 -0
  240. package/dist/vendor/cjs/node/NodeHttpClient.js +1 -0
  241. package/dist/vendor/cjs/node/NodeHttpIncomingMessage.d.ts +62 -0
  242. package/dist/vendor/cjs/node/NodeHttpIncomingMessage.js +1 -0
  243. package/dist/vendor/cjs/node/NodeHttpPlatform.d.ts +36 -0
  244. package/dist/vendor/cjs/node/NodeHttpPlatform.js +1 -0
  245. package/dist/vendor/cjs/node/NodeHttpServer.d.ts +120 -0
  246. package/dist/vendor/cjs/node/NodeHttpServer.js +1 -0
  247. package/dist/vendor/cjs/node/NodeHttpServerRequest.d.ts +29 -0
  248. package/dist/vendor/cjs/node/NodeHttpServerRequest.js +1 -0
  249. package/dist/vendor/cjs/node/NodeMultipart.d.ts +46 -0
  250. package/dist/vendor/cjs/node/NodeMultipart.js +1 -0
  251. package/dist/vendor/cjs/node/NodeMultipartParser.d.ts +97 -0
  252. package/dist/vendor/cjs/node/NodeMultipartParser.js +1 -0
  253. package/dist/vendor/cjs/node/NodePath.d.ts +27 -0
  254. package/dist/vendor/cjs/node/NodePath.js +1 -0
  255. package/dist/vendor/cjs/node/NodeRedis.d.ts +74 -0
  256. package/dist/vendor/cjs/node/NodeRedis.js +1 -0
  257. package/dist/vendor/cjs/node/NodeRuntime.d.ts +85 -0
  258. package/dist/vendor/cjs/node/NodeRuntime.js +1 -0
  259. package/dist/vendor/cjs/node/NodeServices.d.ts +34 -0
  260. package/dist/vendor/cjs/node/NodeServices.js +1 -0
  261. package/dist/vendor/cjs/node/NodeSink.d.ts +8 -0
  262. package/dist/vendor/cjs/node/NodeSink.js +1 -0
  263. package/dist/vendor/cjs/node/NodeSocket.d.ts +38 -0
  264. package/dist/vendor/cjs/node/NodeSocket.js +1 -0
  265. package/dist/vendor/cjs/node/NodeSocketServer.d.ts +8 -0
  266. package/dist/vendor/cjs/node/NodeSocketServer.js +1 -0
  267. package/dist/vendor/cjs/node/NodeStdio.d.ts +11 -0
  268. package/dist/vendor/cjs/node/NodeStdio.js +1 -0
  269. package/dist/vendor/cjs/node/NodeStream.d.ts +8 -0
  270. package/dist/vendor/cjs/node/NodeStream.js +1 -0
  271. package/dist/vendor/cjs/node/NodeTerminal.d.ts +21 -0
  272. package/dist/vendor/cjs/node/NodeTerminal.js +1 -0
  273. package/dist/vendor/cjs/node/NodeWorker.d.ts +22 -0
  274. package/dist/vendor/cjs/node/NodeWorker.js +1 -0
  275. package/dist/vendor/cjs/node/NodeWorkerRunner.d.ts +12 -0
  276. package/dist/vendor/cjs/node/NodeWorkerRunner.js +1 -0
  277. package/dist/vendor/cjs/node/Undici.d.ts +28 -0
  278. package/dist/vendor/cjs/node/Undici.js +1 -0
  279. package/dist/vendor/cjs/node/index.d.ts +100 -0
  280. package/dist/vendor/cjs/node/index.js +1 -0
  281. package/dist/vendor/cjs/node-shared/NodeChildProcessSpawner.d.ts +57 -0
  282. package/dist/vendor/cjs/node-shared/NodeChildProcessSpawner.js +1 -0
  283. package/dist/vendor/cjs/node-shared/NodeClusterSocket.d.ts +22 -0
  284. package/dist/vendor/cjs/node-shared/NodeClusterSocket.js +1 -0
  285. package/dist/vendor/cjs/node-shared/NodeCrypto.d.ts +27 -0
  286. package/dist/vendor/cjs/node-shared/NodeCrypto.js +1 -0
  287. package/dist/vendor/cjs/node-shared/NodeFileSystem.d.ts +11 -0
  288. package/dist/vendor/cjs/node-shared/NodeFileSystem.js +1 -0
  289. package/dist/vendor/cjs/node-shared/NodeHttpCompression.d.ts +36 -0
  290. package/dist/vendor/cjs/node-shared/NodeHttpCompression.js +1 -0
  291. package/dist/vendor/cjs/node-shared/NodePath.d.ts +27 -0
  292. package/dist/vendor/cjs/node-shared/NodePath.js +1 -0
  293. package/dist/vendor/cjs/node-shared/NodeRuntime.d.ts +47 -0
  294. package/dist/vendor/cjs/node-shared/NodeRuntime.js +1 -0
  295. package/dist/vendor/cjs/node-shared/NodeSink.d.ts +69 -0
  296. package/dist/vendor/cjs/node-shared/NodeSink.js +1 -0
  297. package/dist/vendor/cjs/node-shared/NodeSocket.d.ts +134 -0
  298. package/dist/vendor/cjs/node-shared/NodeSocket.js +1 -0
  299. package/dist/vendor/cjs/node-shared/NodeSocketServer.d.ts +80 -0
  300. package/dist/vendor/cjs/node-shared/NodeSocketServer.js +1 -0
  301. package/dist/vendor/cjs/node-shared/NodeStdio.d.ts +12 -0
  302. package/dist/vendor/cjs/node-shared/NodeStdio.js +1 -0
  303. package/dist/vendor/cjs/node-shared/NodeStream.d.ts +179 -0
  304. package/dist/vendor/cjs/node-shared/NodeStream.js +1 -0
  305. package/dist/vendor/cjs/node-shared/NodeTerminal.d.ts +22 -0
  306. package/dist/vendor/cjs/node-shared/NodeTerminal.js +1 -0
  307. package/dist/vendor/cjs/node-shared/index.d.ts +56 -0
  308. package/dist/vendor/cjs/node-shared/index.js +1 -0
  309. package/dist/vendor/cjs/node-shared/internal/nodeChildProcessSpawner.d.ts +4 -0
  310. package/dist/vendor/cjs/node-shared/internal/nodeChildProcessSpawner.js +1 -0
  311. package/dist/vendor/cjs/node-shared/internal/utils.d.ts +2 -0
  312. package/dist/vendor/cjs/node-shared/internal/utils.js +1 -0
  313. package/dist/vendor/cjs/package.json +1 -0
  314. package/dist/vendor/cjs/platform-node-shared.d.ts +56 -0
  315. package/dist/vendor/cjs/platform-node-shared.js +1 -0
  316. package/dist/vendor/cjs/platform-node.d.ts +100 -0
  317. package/dist/vendor/cjs/platform-node.js +1 -0
  318. package/dist/vendor/node/LICENSE +21 -0
  319. package/dist/vendor/node/NodeChildProcessSpawner.d.ts +7 -0
  320. package/dist/vendor/node/NodeChildProcessSpawner.js +14 -0
  321. package/dist/vendor/node/NodeClusterHttp.d.ts +63 -0
  322. package/dist/vendor/node/NodeClusterHttp.js +43 -0
  323. package/dist/vendor/node/NodeClusterSocket.d.ts +79 -0
  324. package/dist/vendor/node/NodeClusterSocket.js +30 -0
  325. package/dist/vendor/node/NodeCrypto.d.ts +10 -0
  326. package/dist/vendor/node/NodeCrypto.js +8 -0
  327. package/dist/vendor/node/NodeFileSystem.d.ts +10 -0
  328. package/dist/vendor/node/NodeFileSystem.js +9 -0
  329. package/dist/vendor/node/NodeHttpClient.d.ts +179 -0
  330. package/dist/vendor/node/NodeHttpClient.js +46 -0
  331. package/dist/vendor/node/NodeHttpIncomingMessage.d.ts +62 -0
  332. package/dist/vendor/node/NodeHttpIncomingMessage.js +10 -0
  333. package/dist/vendor/node/NodeHttpPlatform.d.ts +36 -0
  334. package/dist/vendor/node/NodeHttpPlatform.js +16 -0
  335. package/dist/vendor/node/NodeHttpServer.d.ts +120 -0
  336. package/dist/vendor/node/NodeHttpServer.js +46 -0
  337. package/dist/vendor/node/NodeHttpServerRequest.d.ts +29 -0
  338. package/dist/vendor/node/NodeHttpServerRequest.js +9 -0
  339. package/dist/vendor/node/NodeMultipart.d.ts +46 -0
  340. package/dist/vendor/node/NodeMultipart.js +15 -0
  341. package/dist/vendor/node/NodeMultipartParser.d.ts +97 -0
  342. package/dist/vendor/node/NodeMultipartParser.js +11 -0
  343. package/dist/vendor/node/NodePath.d.ts +27 -0
  344. package/dist/vendor/node/NodePath.js +12 -0
  345. package/dist/vendor/node/NodeRedis.d.ts +74 -0
  346. package/dist/vendor/node/NodeRedis.js +11 -0
  347. package/dist/vendor/node/NodeRuntime.d.ts +85 -0
  348. package/dist/vendor/node/NodeRuntime.js +8 -0
  349. package/dist/vendor/node/NodeServices.d.ts +34 -0
  350. package/dist/vendor/node/NodeServices.js +23 -0
  351. package/dist/vendor/node/NodeSink.d.ts +8 -0
  352. package/dist/vendor/node/NodeSink.js +12 -0
  353. package/dist/vendor/node/NodeSocket.d.ts +38 -0
  354. package/dist/vendor/node/NodeSocket.js +31 -0
  355. package/dist/vendor/node/NodeSocketServer.d.ts +8 -0
  356. package/dist/vendor/node/NodeSocketServer.js +21 -0
  357. package/dist/vendor/node/NodeStdio.d.ts +11 -0
  358. package/dist/vendor/node/NodeStdio.js +10 -0
  359. package/dist/vendor/node/NodeStream.d.ts +8 -0
  360. package/dist/vendor/node/NodeStream.js +27 -0
  361. package/dist/vendor/node/NodeTerminal.d.ts +21 -0
  362. package/dist/vendor/node/NodeTerminal.js +10 -0
  363. package/dist/vendor/node/NodeWorker.d.ts +22 -0
  364. package/dist/vendor/node/NodeWorker.js +9 -0
  365. package/dist/vendor/node/NodeWorkerRunner.d.ts +12 -0
  366. package/dist/vendor/node/NodeWorkerRunner.js +7 -0
  367. package/dist/vendor/node/Undici.d.ts +28 -0
  368. package/dist/vendor/node/Undici.js +9 -0
  369. package/dist/vendor/node/index.d.ts +100 -0
  370. package/dist/vendor/node/index.js +115 -0
  371. package/dist/vendor/node-shared/LICENSE +21 -0
  372. package/dist/vendor/node-shared/NodeChildProcessSpawner.d.ts +57 -0
  373. package/dist/vendor/node-shared/NodeChildProcessSpawner.js +13 -0
  374. package/dist/vendor/node-shared/NodeClusterSocket.d.ts +22 -0
  375. package/dist/vendor/node-shared/NodeClusterSocket.js +11 -0
  376. package/dist/vendor/node-shared/NodeCrypto.d.ts +27 -0
  377. package/dist/vendor/node-shared/NodeCrypto.js +9 -0
  378. package/dist/vendor/node-shared/NodeFileSystem.d.ts +11 -0
  379. package/dist/vendor/node-shared/NodeFileSystem.js +8 -0
  380. package/dist/vendor/node-shared/NodeHttpCompression.d.ts +36 -0
  381. package/dist/vendor/node-shared/NodeHttpCompression.js +13 -0
  382. package/dist/vendor/node-shared/NodePath.d.ts +27 -0
  383. package/dist/vendor/node-shared/NodePath.js +11 -0
  384. package/dist/vendor/node-shared/NodeRuntime.d.ts +47 -0
  385. package/dist/vendor/node-shared/NodeRuntime.js +7 -0
  386. package/dist/vendor/node-shared/NodeSink.d.ts +69 -0
  387. package/dist/vendor/node-shared/NodeSink.js +11 -0
  388. package/dist/vendor/node-shared/NodeSocket.d.ts +134 -0
  389. package/dist/vendor/node-shared/NodeSocket.js +23 -0
  390. package/dist/vendor/node-shared/NodeSocketServer.d.ts +80 -0
  391. package/dist/vendor/node-shared/NodeSocketServer.js +20 -0
  392. package/dist/vendor/node-shared/NodeStdio.d.ts +12 -0
  393. package/dist/vendor/node-shared/NodeStdio.js +9 -0
  394. package/dist/vendor/node-shared/NodeStream.d.ts +179 -0
  395. package/dist/vendor/node-shared/NodeStream.js +26 -0
  396. package/dist/vendor/node-shared/NodeTerminal.d.ts +22 -0
  397. package/dist/vendor/node-shared/NodeTerminal.js +9 -0
  398. package/dist/vendor/node-shared/index.d.ts +56 -0
  399. package/dist/vendor/node-shared/index.js +58 -0
  400. package/dist/vendor/node-shared/internal/nodeChildProcessSpawner.d.ts +4 -0
  401. package/dist/vendor/node-shared/internal/nodeChildProcessSpawner.js +7 -0
  402. package/dist/vendor/node-shared/internal/utils.d.ts +2 -0
  403. package/dist/vendor/node-shared/internal/utils.js +7 -0
  404. package/dist/vendor/platform-node-shared.d.ts +56 -0
  405. package/dist/vendor/platform-node-shared.js +58 -0
  406. package/dist/vendor/platform-node.d.ts +100 -0
  407. package/dist/vendor/platform-node.js +115 -0
  408. package/docs/README.md +129 -0
  409. package/docs/api.md +552 -0
  410. package/docs/concepts/descriptor-relative-filesystem.md +181 -0
  411. package/docs/concepts/host-bundle.md +125 -0
  412. package/docs/concepts/process-containment.md +182 -0
  413. package/docs/guides/answer-run-ownership.md +87 -0
  414. package/docs/guides/configure-the-filesystem-helper.md +152 -0
  415. package/docs/guides/contain-child-processes.md +139 -0
  416. package/docs/guides/match-files-with-glob.md +151 -0
  417. package/docs/installation.md +204 -0
  418. package/docs/quickstart.md +147 -0
  419. package/docs/troubleshooting.md +130 -0
  420. package/package.json +202 -3
  421. package/src/AtomicFileSystem.ts +415 -0
  422. package/src/EgressHttpClient.ts +327 -0
  423. package/src/HostLiveness.ts +103 -0
  424. package/src/NodeHost.ts +253 -0
  425. package/src/ProcessConfinement.ts +356 -0
  426. package/src/ProcessReaper.ts +951 -0
  427. package/src/ProcessSandbox.ts +1273 -0
  428. package/src/ScopedProcess.ts +123 -0
  429. package/src/index.ts +44 -0
  430. package/src/internal/AtomicFileSystemExecutable.ts +379 -0
  431. package/src/internal/AtomicFileSystemProtocol.ts +502 -0
  432. package/src/internal/AtomicFileSystemTransport.ts +272 -0
  433. package/src/internal/AtomicHelperError.ts +50 -0
  434. package/src/internal/AtomicHelperFault.ts +84 -0
  435. package/src/internal/PipedProcess.ts +357 -0
  436. package/src/internal/ProcSnapshot.ts +193 -0
  437. package/src/internal/ProcessCleanup.ts +99 -0
  438. package/src/internal/ProcessCleanupPolicyError.ts +30 -0
  439. package/src/internal/ProcessFault.ts +82 -0
  440. package/src/internal/ProcessSupervisor.ts +801 -0
  441. package/src/internal/SupervisorProgram.ts +289 -0
  442. package/src/internal/WindowsProcessJob.ts +114 -0
@@ -0,0 +1,951 @@
1
+ /**
2
+ * Reaping the processes a dead host incarnation abandoned.
3
+ *
4
+ * Scope closure cleans up work a live host still owns. On POSIX, a prepared
5
+ * supervisor also receives private-channel EOF when its host disappears and
6
+ * independently requests group cleanup. The durable ledger remains the fallback
7
+ * when that cleanup is incomplete or cannot be verified; the next incarnation
8
+ * uses this module to examine the recorded process identities before acting.
9
+ *
10
+ * The rules are narrow on purpose, because the input is a durable record that
11
+ * outlived the process that wrote it and a pid is reused. A record is signalled
12
+ * only when ALL of these hold:
13
+ *
14
+ * - it belongs to a DIFFERENT incarnation of the SAME `hostId`
15
+ * ({@link ProcessLedger.Service.orphans} already filters that);
16
+ * - its owner is gone. A pid that exists but belongs to another user counts as
17
+ * alive, exactly as {@link HostLiveness} counts it, so only `ESRCH` reads as
18
+ * dead and every other answer keeps the record;
19
+ * - it names a target the platform can actually signal. On POSIX that is a
20
+ * process group the child leads itself, so `pgid` has to be a safe integer
21
+ * above 1 and equal to `pid`; on Windows it is a pid, so `pgid` has to be
22
+ * absent. The check is made against the DURABLE record rather than trusted
23
+ * from it: `process.kill(-0, "SIGKILL")` signals this host's own process
24
+ * group, and `@smthrs/kernel`'s ledger schema admits zero;
25
+ * - that group is not THIS process's group, and its pid is not this process's
26
+ * pid. The host's real process group is read from the operating system,
27
+ * because a stored number can claim anything and the shell that started this
28
+ * host shares its group. A host that cannot read its own group keeps the
29
+ * record rather than signalling it on a guard that did not run;
30
+ * - the number still names the process the record describes. A record written
31
+ * before this machine booted names a pid from a pid space that no longer
32
+ * exists, and within one boot the recorded start time has to match the start
33
+ * time the operating system reports for that pid. A host that cannot ASK the
34
+ * question answers {@link StartTime} `unavailable`, which refuses the kill
35
+ * and keeps the record; no evidence never authorizes a signal.
36
+ *
37
+ * The outcome of the signal is part of the record. A kill that succeeds, or
38
+ * that reports the group is already gone, retires the record through
39
+ * {@link ProcessLedger.Service.reaped}; a kill that FAILS for any other reason
40
+ * leaves the record inherited so the next incarnation tries again. A record
41
+ * refused on grounds the operating system will not revisit — a number no
42
+ * platform can signal, a pid that is gone, a start time that does not match — is
43
+ * retired through {@link ProcessLedger.Service.skipped}, which says in the
44
+ * journal that nothing was signalled and stops every later incarnation
45
+ * re-examining a number the operating system has moved on from. The three
46
+ * refusals a later incarnation CAN answer differently — `owner-alive`,
47
+ * `identity-unverified`, and `own-group-unknown` — retire nothing.
48
+ *
49
+ * @since 0.1.0
50
+ */
51
+
52
+ import * as ContainedSpawner from "@smthrs/kernel/ContainedSpawner"
53
+ import * as ProcessLedger from "@smthrs/kernel/ProcessLedger"
54
+ import * as Effect from "effect/Effect"
55
+ import * as Layer from "effect/Layer"
56
+ import * as Metric from "effect/Metric"
57
+ import type * as ChildProcess from "effect/unstable/process/ChildProcess"
58
+ import { ChildProcessSpawner, make as makeSpawner } from "effect/unstable/process/ChildProcessSpawner"
59
+ import { spawnSync } from "node:child_process"
60
+ import { uptime } from "node:os"
61
+ import { parse, win32 } from "node:path"
62
+ import { packageRoot, resolveDefaultExecutable } from "./internal/AtomicFileSystemExecutable.ts"
63
+ import { usableExecutable } from "./internal/AtomicFileSystemTransport.ts"
64
+ import * as PipedProcess from "./internal/PipedProcess.ts"
65
+ import * as ProcessCleanup from "./internal/ProcessCleanup.ts"
66
+ import * as ProcSnapshot from "./internal/ProcSnapshot.ts"
67
+
68
+ /**
69
+ * What the operating system can say about a pid.
70
+ *
71
+ * `unknown` is not `dead`. A pid this host may not signal answers `EPERM`, and
72
+ * a reaper that read that as "gone" would kill another user's children on the
73
+ * strength of a record it cannot verify.
74
+ *
75
+ * @category models
76
+ * @since 0.1.0
77
+ */
78
+ export type Liveness = "alive" | "dead" | "unknown"
79
+
80
+ /**
81
+ * What happened when a process group was signalled.
82
+ *
83
+ * @category models
84
+ * @since 0.1.0
85
+ */
86
+ export type KillOutcome =
87
+ /** The signal was delivered. */
88
+ | "signalled"
89
+ /** There was nothing left to signal, which is the same end state. */
90
+ | "already-gone"
91
+ /** The signal was refused, and the record must be tried again later. */
92
+ | "failed"
93
+
94
+ /**
95
+ * What the operating system can say about when a pid started.
96
+ *
97
+ * The three negative answers are deliberately distinct, because they authorize
98
+ * three different decisions. `gone` is evidence — the pid does not exist, so
99
+ * the record describes nothing and is retired unsignalled. `unavailable` is the
100
+ * ABSENCE of evidence: `ps` is missing, unanswerable, or printed something no
101
+ * clock can read, so the record is kept for an incarnation that can ask.
102
+ * `unsupported` is a platform that has no such column at all, which falls back
103
+ * to the boot-time comparison alone.
104
+ *
105
+ * @category models
106
+ * @since 1.0.0-rc.0
107
+ */
108
+ export type StartTime =
109
+ /** The operating system reports this start time, in epoch milliseconds. */
110
+ | { readonly _tag: "started"; readonly startedAtMs: number }
111
+ /** The pid names no process. */
112
+ | { readonly _tag: "gone" }
113
+ /** The question could not be asked on this host. */
114
+ | { readonly _tag: "unavailable" }
115
+ /** The platform has no start-time column at all. */
116
+ | { readonly _tag: "unsupported" }
117
+
118
+ /**
119
+ * The operating-system operations reaping needs.
120
+ *
121
+ * It is a seam, not an abstraction: `flows` ships exactly the two
122
+ * implementations below and a test drives the Windows one on a POSIX host,
123
+ * which is the only way that path is ever exercised in this repository.
124
+ *
125
+ * @category models
126
+ * @since 0.1.0
127
+ */
128
+ export interface System {
129
+ /** Whether a pid names a process that still exists. */
130
+ readonly isAlive: (pid: number) => Liveness
131
+ /** When the process behind a pid started. */
132
+ readonly startedAtMs: (pid: number) => StartTime
133
+ /** The process group THIS process belongs to, or `null` where there are none. */
134
+ readonly ownGroup: () => number | null
135
+ /** When this machine booted, in epoch milliseconds. */
136
+ readonly bootedAtMs: () => number
137
+ /**
138
+ * Whether the numbers in a record name something this platform may signal,
139
+ * checked before any other guard consults them.
140
+ */
141
+ readonly refuseTarget: (record: ProcessLedger.ProcessRecord) => Refusal | undefined
142
+ /** Ends an abandoned process and everything below it. */
143
+ readonly killTree: (record: ProcessLedger.ProcessRecord) => KillOutcome
144
+ }
145
+
146
+ const errorCode = (cause: unknown): string | undefined => (cause as { readonly code?: string } | null)?.code
147
+
148
+ const liveness = (pid: number): Liveness => {
149
+ try {
150
+ // Signal 0 delivers nothing; it only asks whether the pid is signalable.
151
+ process.kill(pid, 0)
152
+ return "alive"
153
+ } catch (cause) {
154
+ // ESRCH is the only answer that means gone. EPERM means the process
155
+ // exists and belongs to another user; anything else is a question this
156
+ // host could not ask, and both keep the record rather than kill on it.
157
+ return errorCode(cause) === "ESRCH" ? "dead" : "unknown"
158
+ }
159
+ }
160
+
161
+ const bootedAtMs = (): number => Date.now() - Math.round(uptime() * 1000)
162
+
163
+ /**
164
+ * The start time `ps` reports for a pid.
165
+ *
166
+ * `lstart` is the one start-time column both BSD (macOS) and procps (Linux)
167
+ * spell the same way, and it prints an absolute time rather than an elapsed
168
+ * one, so the answer does not drift between reading it and comparing it. It is
169
+ * also a LOCALIZED column, so the probe pins `LC_ALL=C` and `TZ=UTC`.
170
+ * Its text has no zone; parsing explicitly as UTC keeps the identity independent
171
+ * of the host runtime's timezone.
172
+ */
173
+ const startedAtMs = (executable: string) => (pid: number): StartTime => {
174
+ const answer = ps(executable, "lstart", pid)
175
+ if (answer._tag === "gone") {
176
+ // A nonzero exit is `ps` saying the pid does not exist, but it is also what
177
+ // a `ps` that rejected the column, or one that is not the `ps` this probe
178
+ // expects, says about a pid that is very much alive. The kernel is asked
179
+ // directly before a live process is retired as gone.
180
+ return liveness(pid) === "dead" ? answer : { _tag: "unavailable" }
181
+ }
182
+ if (answer._tag !== "printed") return answer
183
+ const parsed = Date.parse(`${answer.text} UTC`)
184
+ return Number.isNaN(parsed) ? { _tag: "unavailable" } : { _tag: "started", startedAtMs: parsed }
185
+ }
186
+
187
+ /**
188
+ * The absolute path the process probe is run from.
189
+ *
190
+ * Never a `PATH` lookup. The inherited environment belongs to whoever started
191
+ * this host, and a planted `ps` that prints a fabricated start time — or none
192
+ * at all — would decide whether a `SIGKILL` is authorized.
193
+ *
194
+ * @category constants
195
+ * @since 1.0.0-rc.0
196
+ */
197
+ export const defaultPsExecutable = "/bin/ps"
198
+
199
+ /** How long the probe may take before it counts as an answer nobody gave. */
200
+ const psTimeoutMs = 5000
201
+
202
+ /**
203
+ * The Windows directory the host was started with, when it names one.
204
+ */
205
+ const windowsRoot = (): string => {
206
+ const systemRoot = process.env.SystemRoot
207
+ return systemRoot !== undefined && win32.isAbsolute(systemRoot) ? systemRoot : "C:\\Windows"
208
+ }
209
+
210
+ /**
211
+ * The system `taskkill`, by absolute path. Windows resolves a bare name from
212
+ * the current directory before `PATH`, so a flow that wrote `taskkill.exe`
213
+ * into the host's working directory would otherwise run as the host.
214
+ */
215
+ const taskkillExecutable = (): string => win32.join(windowsRoot(), "System32", "taskkill.exe")
216
+
217
+ /** A live-release probe has its own short bound within the shutdown deadline. */
218
+ const cleanupProbeTimeoutMs = 2000
219
+
220
+ /**
221
+ * One fixed-path snapshot supplies group membership, start identities and our
222
+ * own group together. Only numeric fields and C-locale lstart are read; command
223
+ * text and the caller's PATH never influence signal authority.
224
+ */
225
+ const psGroupSnapshot = (pgid: number): ProcessCleanup.Snapshot | undefined => {
226
+ const result = spawnSync(defaultPsExecutable, ["-A", "-o", "pid=,pgid=,stat=,lstart="], {
227
+ encoding: "utf8",
228
+ timeout: cleanupProbeTimeoutMs,
229
+ killSignal: "SIGKILL",
230
+ env: { LC_ALL: "C", TZ: "UTC", PATH: "/usr/bin:/bin" }
231
+ })
232
+ if (result.error !== undefined || result.signal !== null || result.status !== 0) return undefined
233
+ let own: number | undefined
234
+ const members: Array<ProcessCleanup.Member> = []
235
+ for (const line of result.stdout.trim().split("\n")) {
236
+ const match = /^\s*([0-9]+)\s+([0-9]+)\s+(\S+)\s+(.+)$/.exec(line)
237
+ if (match === null) return undefined
238
+ const pid = Number(match[1])
239
+ const group = Number(match[2])
240
+ if (!Number.isSafeInteger(pid) || !Number.isSafeInteger(group)) return undefined
241
+ if (pid === process.pid) own = group
242
+ if (group !== pgid) continue
243
+ const started = Date.parse(`${match[4]!} UTC`)
244
+ if (Number.isNaN(started)) return undefined
245
+ members.push({ pid, startedAtMs: started, zombie: match[3]!.startsWith("Z") })
246
+ }
247
+ return own === undefined || own <= 0 ? undefined : { ownGroup: own, members }
248
+ }
249
+
250
+ /**
251
+ * The observation the supervisor polls, from the source that platform has.
252
+ *
253
+ * Linux publishes the whole process table as files under `/proc`, which is
254
+ * where `ps` itself reads it, so the kernel is asked directly. That needs no
255
+ * `procps` in the image, cannot be answered by a planted binary, and costs no
256
+ * fork on a poll that runs every 10ms. A missing `/bin/ps` is not a
257
+ * hypothetical: `procps` is not in `debian:bookworm-slim`, and on Cloud CI run
258
+ * 11727 every spawned target in every task finished its work and then failed
259
+ * cleanup verification with `Unknown: ChildProcess.kill`, because this
260
+ * observation could not be made.
261
+ *
262
+ * Every other POSIX host keeps the `ps` reading, which is the only portable
263
+ * spelling of this question off Linux.
264
+ *
265
+ * @category constructors
266
+ * @since 1.0.0
267
+ */
268
+ export const groupSnapshotFor = (platform: string): (pgid: number) => ProcessCleanup.Snapshot | undefined =>
269
+ platform === "linux" ? ProcSnapshot.snapshot(ProcSnapshot.defaultProcRoot) : psGroupSnapshot
270
+
271
+ /**
272
+ * Whether the kernel reports no process at all in a group.
273
+ *
274
+ * Signal 0 delivers nothing; it only asks the kernel whether the group has a
275
+ * member. `ESRCH` is the one answer that proves the group empty, with no fork
276
+ * and no process table to parse, so it still answers on a loaded host where a
277
+ * `ps` snapshot cannot be taken. A live member, a zombie (macOS answers
278
+ * `EPERM`, Linux succeeds), another user's member or any other error answers
279
+ * `false`, which leaves the question to the snapshot.
280
+ *
281
+ * @category constructors
282
+ * @since 1.0.0
283
+ */
284
+ export const groupVacant = (pgid: number): boolean => {
285
+ // kill(0) and kill(-1) address the caller's group and every process.
286
+ if (!Number.isSafeInteger(pgid) || pgid <= 1) return false
287
+ try {
288
+ process.kill(-pgid, 0)
289
+ return false
290
+ } catch (cause) {
291
+ return errorCode(cause) === "ESRCH"
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Live cleanup for Node and Bun contained spawners.
297
+ *
298
+ * A trusted supervisor remains the group owner after the target exits. It
299
+ * enforces cleanup on target completion, parent loss and explicit release; the
300
+ * target starts only after its owner is durably recorded. Host observations
301
+ * never signal a pid: they only shorten the owner's deadline or verify an empty
302
+ * group before retiring its record. Failed verification retains the record.
303
+ *
304
+ * @category constructors
305
+ * @since 1.0.0
306
+ */
307
+ export const processLifecycle = ProcessCleanup.lifecycle({
308
+ platform: process.platform,
309
+ snapshot: groupSnapshotFor(process.platform),
310
+ vacant: groupVacant
311
+ })
312
+
313
+ /**
314
+ * The default grace period for a contained host spawner.
315
+ *
316
+ * The platform is always the running platform; callers cannot describe a
317
+ * Windows ledger identity for a process that actually leads a POSIX group.
318
+ * @category models
319
+ * @since 1.0.0
320
+ */
321
+ export type SpawnerOptions = Omit<ContainedSpawner.Options, "platform">
322
+
323
+ /**
324
+ * Provides a ledger-backed spawner with this platform's prepared cleanup.
325
+ *
326
+ * Native pipe listeners live until their descriptors close, including writes
327
+ * interrupted by owner loss. The kernel expands pipelines before this private
328
+ * standard-command adapter is called. Windows owners join a native job before
329
+ * activation; POSIX owners enforce their process-group cleanup deadline.
330
+ *
331
+ * The caller supplies the durable or in-memory ledger. This layer records new
332
+ * owners; {@link layer} separately reaps
333
+ * records inherited from a previous host incarnation.
334
+ * @category layers
335
+ * @since 1.0.0
336
+ */
337
+ export const layerSpawner = (
338
+ options?: SpawnerOptions
339
+ ): Layer.Layer<ChildProcessSpawner, never, ProcessLedger.ProcessLedger> => {
340
+ const contained = ContainedSpawner.layer({ graceMs: options?.graceMs, platform: process.platform }, processLifecycle)
341
+ const standard = makeSpawner((command) =>
342
+ PipedProcess.spawn(command as ChildProcess.StandardCommand, undefined, true)
343
+ )
344
+ return Layer.provide(contained, Layer.succeed(ChildProcessSpawner)(standard))
345
+ }
346
+
347
+ /** A whole number and nothing else, which is all a `pgid` column may be. */
348
+ const decimal = /^[0-9]+$/
349
+
350
+ /**
351
+ * One `ps` column for one pid.
352
+ *
353
+ * `printed` is an answer; every other tag is the absence of one. A nonzero exit
354
+ * with no error is `ps` saying the pid does not exist, which is evidence; a
355
+ * spawn that failed, a signal, or a timeout is not.
356
+ */
357
+ const ps = (
358
+ executable: string,
359
+ column: string,
360
+ pid: number
361
+ ): { readonly _tag: "printed"; readonly text: string } | StartTime => {
362
+ const result = spawnSync(executable, ["-o", `${column}=`, "-p", String(pid)], {
363
+ encoding: "utf8",
364
+ timeout: psTimeoutMs,
365
+ // `SIGKILL` rather than the default `SIGTERM`, because this call blocks the
366
+ // event loop while a host is being built: `spawnSync` keeps waiting past
367
+ // the deadline for a process that ignores the signal, so a `TERM` the probe
368
+ // can decline is not a deadline at all.
369
+ killSignal: "SIGKILL",
370
+ // An empty-ish environment for the same reason the helper in
371
+ // `AtomicFileSystem` gets one: nothing the caller exported may change what
372
+ // this answers. Pin both the column's spelling and its timezone; the parser
373
+ // supplies UTC explicitly because `lstart` does not print a zone.
374
+ env: { LC_ALL: "C", TZ: "UTC", PATH: "/usr/bin:/bin" }
375
+ })
376
+ if (result.error !== undefined || result.signal !== null || result.status === null) {
377
+ return { _tag: "unavailable" }
378
+ }
379
+ if (result.status !== 0) return { _tag: "gone" }
380
+ const printed = result.stdout.trim()
381
+ return printed === "" ? { _tag: "unavailable" } : { _tag: "printed", text: printed }
382
+ }
383
+
384
+ /**
385
+ * This process's own process group.
386
+ *
387
+ * Node exposes no `getpgrp`, so the answer comes from `ps`, which both BSD and
388
+ * procps spell `pgid`. A host that cannot read it answers `null` and loses one
389
+ * guard, which is why the pid comparison below is kept as well.
390
+ *
391
+ * The text is required to be one run of decimal digits naming a real group,
392
+ * rather than run through `Number.parseInt`, which reads a PREFIX: `"12 34"`,
393
+ * `"12abc"`, and `"0x10"` all become a number that is not this host's group. A
394
+ * wrong number is worse here than no number at all. It silently passes the
395
+ * own-group comparison in {@link refuse}, and because it is not `null` it also
396
+ * suppresses the `own-group-unknown` refusal that exists to say the comparison
397
+ * could not be made, so the guard neither runs nor reports that it did not run
398
+ * and a `SIGKILL` goes out on it. Anything else answers `null`, which refuses.
399
+ */
400
+ const ownGroup = (executable: string) => (): number | null => {
401
+ const answer = ps(executable, "pgid", process.pid)
402
+ if (answer._tag !== "printed") return null
403
+ if (!decimal.test(answer.text)) return null
404
+ const parsed = Number(answer.text)
405
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null
406
+ }
407
+
408
+ /**
409
+ * How a {@link System} asks the operating system its questions, and whose
410
+ * identity it must never signal.
411
+ *
412
+ * @category models
413
+ * @since 1.0.0-rc.0
414
+ */
415
+ export interface SystemOptions {
416
+ /**
417
+ * The absolute `ps` the identity probe runs. Default
418
+ * {@link defaultPsExecutable}. It is configuration for a host that installs
419
+ * `ps` somewhere else, and the seam the probe's own parsing is driven
420
+ * through; it is never a `PATH` lookup. Windows has no such probe and
421
+ * ignores it.
422
+ */
423
+ readonly psExecutable?: string | undefined
424
+ /** Absolute native helper for Windows process identity queries; never a PATH lookup. */
425
+ readonly processExecutable?: string | undefined
426
+ /**
427
+ * The pid this system must never signal, nor signal the group of. Default
428
+ * `process.pid`. {@link Options.ownerPid} is the same identity at the sweep
429
+ * level; this one is what {@link System.killTree} enforces on its own,
430
+ * because that function is exported and a caller can reach it without going
431
+ * through {@link reap}.
432
+ */
433
+ readonly ownerPid?: number | undefined
434
+ }
435
+
436
+ /**
437
+ * Whether a durable record names a POSIX process group this host may signal.
438
+ *
439
+ * `process.kill(-pgid, ...)` turns the stored number into a target directly, so
440
+ * every value that is not a real foreign group has to be refused before it gets
441
+ * there: `0` signals THIS process's group, a negative value stops naming a
442
+ * group at all and names a single pid, and a `pgid` that differs from `pid`
443
+ * describes a child that does not lead the group it claims —
444
+ * `ContainedSpawner.groupOf` writes `pgid === pid` for every detached POSIX
445
+ * child, so anything else is a record no writer in this repository produced.
446
+ */
447
+ const refusePosixTarget = (record: ProcessLedger.ProcessRecord): Refusal | undefined => {
448
+ if (record.pgid === null) return "no-group"
449
+ if (!Number.isSafeInteger(record.pid) || record.pid <= 1) return "invalid-record"
450
+ if (!Number.isSafeInteger(record.pgid) || record.pgid <= 1) return "invalid-record"
451
+ return record.pgid === record.pid ? undefined : "invalid-record"
452
+ }
453
+
454
+ /**
455
+ * Whether a durable record names a Windows process tree this host may signal.
456
+ *
457
+ * Windows has no process groups, so `ContainedSpawner.groupOf` records `null`
458
+ * there and `taskkill /T` walks the tree from the pid instead. A win32 record
459
+ * carrying a number is therefore a record some other platform wrote.
460
+ */
461
+ const refuseWindowsTarget = (record: ProcessLedger.ProcessRecord): Refusal | undefined => {
462
+ if (record.pgid !== null) return "invalid-record"
463
+ return Number.isSafeInteger(record.pid) && record.pid > 1 && record.pid <= 0xffff_ffff ? undefined : "invalid-record"
464
+ }
465
+
466
+ /**
467
+ * Reaping on a POSIX host, with the identity probe configured.
468
+ *
469
+ * The group is killed outright rather than asked politely first. `SIGTERM`
470
+ * buys a graceful shutdown only when someone is left to wait for it, and the
471
+ * incarnation that owned these processes is by definition gone.
472
+ *
473
+ * @category constructors
474
+ * @since 1.0.0-rc.0
475
+ */
476
+ export const posixSystemWith = (options?: SystemOptions): System =>
477
+ posixSystemFrom({
478
+ startedAtMs: startedAtMs(options?.psExecutable ?? defaultPsExecutable),
479
+ ownGroup: ownGroup(options?.psExecutable ?? defaultPsExecutable),
480
+ bootedAtMs
481
+ }, options?.ownerPid ?? process.pid)
482
+
483
+ /**
484
+ * Where a Linux {@link System} reads the process table, and whose identity it
485
+ * must never signal.
486
+ *
487
+ * @category models
488
+ * @since 1.0.0-rc.1
489
+ */
490
+ export interface ProcSystemOptions {
491
+ /** The `/proc` mount the probes read. Default `/proc`. */
492
+ readonly procRoot?: string | undefined
493
+ /** The pid this system must never signal, nor signal the group of. Default `process.pid`. */
494
+ readonly ownerPid?: number | undefined
495
+ }
496
+
497
+ /**
498
+ * Reaping on Linux, with every identity question read from `/proc` rather
499
+ * than from `ps`.
500
+ *
501
+ * `procps` is not in `debian:bookworm-slim` (Cloud CI run 11727), and a
502
+ * `ps`-based reaper on such a host can read neither its own group nor any
503
+ * start time, so it refuses every orphan as `own-group-unknown` on every
504
+ * restart. The live-cleanup observation ({@link groupSnapshotFor}) already
505
+ * reads `/proc` for the same reason; this is the crash-recovery half.
506
+ *
507
+ * A pid with no `/proc/<pid>/stat` is `gone` only when the kernel also says so,
508
+ * because a `hidepid` mount hides another user's live process the same way.
509
+ *
510
+ * @category constructors
511
+ * @since 1.0.0-rc.1
512
+ */
513
+ export const procSystemWith = (options?: ProcSystemOptions): System => {
514
+ const root = options?.procRoot ?? ProcSnapshot.defaultProcRoot
515
+ const readStart = ProcSnapshot.startedAtMs(root)
516
+ return posixSystemFrom({
517
+ startedAtMs: (pid) => {
518
+ const answer = readStart(pid)
519
+ return answer._tag === "gone" && liveness(pid) !== "dead" ? { _tag: "unavailable" } : answer
520
+ },
521
+ ownGroup: ProcSnapshot.ownGroup(root),
522
+ bootedAtMs: () => ProcSnapshot.bootMs(root) ?? bootedAtMs()
523
+ }, options?.ownerPid ?? process.pid)
524
+ }
525
+
526
+ /** The POSIX kill path over whichever probes the platform answers with. */
527
+ const posixSystemFrom = (
528
+ probes: Pick<System, "startedAtMs" | "ownGroup" | "bootedAtMs">,
529
+ ownerPid: number
530
+ ): System => {
531
+ const readOwnGroup = probes.ownGroup
532
+ return {
533
+ isAlive: liveness,
534
+ startedAtMs: probes.startedAtMs,
535
+ ownGroup: readOwnGroup,
536
+ bootedAtMs: probes.bootedAtMs,
537
+ refuseTarget: refusePosixTarget,
538
+ killTree: (record) => {
539
+ // Repeated here and not only in `refuse`: this function is exported, and
540
+ // the negation below is the one place a stored number becomes a signal.
541
+ if (refusePosixTarget(record) !== undefined) return "failed"
542
+ // The host's own identity, repeated for the same reason. `refuse` holds
543
+ // one `ownGroup` reading for the whole sweep; a caller that reaches this
544
+ // function directly has made no such reading, and `process.kill(-pgid)`
545
+ // does not care which way the number arrived. A group that cannot be
546
+ // read is a comparison that did not happen, so nothing is signalled.
547
+ if (record.pid === ownerPid || record.pgid === ownerPid) return "failed"
548
+ const group = readOwnGroup()
549
+ if (group === null || record.pid === group || record.pgid === group) return "failed"
550
+ try {
551
+ process.kill(-(record.pgid as number), "SIGKILL")
552
+ return "signalled"
553
+ } catch (cause) {
554
+ // The group settled between the liveness check and the signal: the end
555
+ // state is the one that was wanted. Anything else is a kill that did NOT
556
+ // happen, and saying so is what gets the record tried again.
557
+ return errorCode(cause) === "ESRCH" ? "already-gone" : "failed"
558
+ }
559
+ }
560
+ }
561
+ }
562
+
563
+ /**
564
+ * Reaping on a POSIX host: one `SIGKILL` to the abandoned process group, with
565
+ * the identity probe at {@link defaultPsExecutable}.
566
+ *
567
+ * @category constructors
568
+ * @since 0.1.0
569
+ */
570
+ export const posixSystem: System = posixSystemWith()
571
+
572
+ /**
573
+ * Reaping on Windows, with the identity the sweep must never signal
574
+ * configured.
575
+ *
576
+ * @category constructors
577
+ * @since 1.0.0-rc.0
578
+ */
579
+ export const windowsSystemWith = (options?: SystemOptions): System => {
580
+ const ownerPid = options?.ownerPid ?? process.pid
581
+ const startedAtMs = (pid: number): StartTime => {
582
+ if (!Number.isSafeInteger(pid) || pid <= 1 || pid > 0xffff_ffff) return { _tag: "unavailable" }
583
+ try {
584
+ const configured = options?.processExecutable ?? process.env.SMITHERS_WORKSPACE_JJ_EXPORT_BINARY
585
+ const executable = configured === undefined
586
+ ? resolveDefaultExecutable(packageRoot, undefined)
587
+ : usableExecutable(configured, undefined)
588
+ const result = spawnSync(executable, ["--process-info", String(pid)], {
589
+ cwd: parse(process.execPath).root,
590
+ env: {},
591
+ encoding: "utf8",
592
+ timeout: psTimeoutMs,
593
+ killSignal: "SIGKILL",
594
+ maxBuffer: 4096
595
+ })
596
+ if (result.error !== undefined || result.signal !== null || result.status !== 0) return { _tag: "unavailable" }
597
+ const value = JSON.parse(result.stdout) as { readonly status?: unknown; readonly startedAtMs?: unknown } | null
598
+ if (value?.status === "gone") return { _tag: "gone" }
599
+ return value?.status === "started" && typeof value.startedAtMs === "number" &&
600
+ Number.isSafeInteger(value.startedAtMs) && value.startedAtMs >= 0
601
+ ? { _tag: "started", startedAtMs: value.startedAtMs }
602
+ : { _tag: "unavailable" }
603
+ } catch {
604
+ return { _tag: "unavailable" }
605
+ }
606
+ }
607
+ return {
608
+ isAlive: liveness,
609
+ startedAtMs,
610
+ ownGroup: () => null,
611
+ bootedAtMs,
612
+ refuseTarget: (record) => record.pid === ownerPid ? "own-group" : refuseWindowsTarget(record),
613
+ killTree: (record) => {
614
+ if (refuseWindowsTarget(record) !== undefined) return "failed"
615
+ // `taskkill /T` walks the tree DOWN from the pid it is given, so a record
616
+ // naming this host takes the host and everything it started with it.
617
+ if (record.pid === ownerPid) return "failed"
618
+ const result = spawnSync(taskkillExecutable(), ["/pid", String(record.pid), "/T", "/F"], {
619
+ // An inert cwd and only the variable Windows system binaries need.
620
+ cwd: parse(process.execPath).root,
621
+ env: { SystemRoot: windowsRoot() },
622
+ stdio: "ignore"
623
+ })
624
+ if (result.error !== undefined) return "failed"
625
+ // 128 is `taskkill`'s "no such process", which is the same end state a
626
+ // successful kill produces.
627
+ if (result.status === 0) return "signalled"
628
+ return result.status === 128 ? "already-gone" : "failed"
629
+ }
630
+ }
631
+ }
632
+
633
+ /**
634
+ * Reaping on Windows, which has no process groups: `taskkill /T /F` by pid.
635
+ *
636
+ * **Windows is outside the 1.0.0-rc.0 support contract**
637
+ * (`docs/pages/installation.md`). Full Windows process containment remains
638
+ * under verification. It is kept — and reachable through {@link reap} rather than only
639
+ * by calling it directly — because a reaper that silently retired every win32
640
+ * record would be the "excluded feature that appears to work partially" the
641
+ * release policy forbids. Treat it as unsupported best-effort.
642
+ *
643
+ * The packaged native helper reads creation time from a pinned process handle.
644
+ * A missing helper, malformed reply, or denied query keeps the durable record
645
+ * for retry. Windows has no process groups, so own-group refusal uses the pid.
646
+ *
647
+ * @category constructors
648
+ * @since 0.1.0
649
+ */
650
+ export const windowsSystem: System = windowsSystemWith()
651
+
652
+ /**
653
+ * Reaping on Linux from the kernel's own `/proc`.
654
+ *
655
+ * @category constructors
656
+ * @since 1.0.0-rc.1
657
+ */
658
+ export const procSystem: System = procSystemWith()
659
+
660
+ /**
661
+ * Selects the reaping implementation a platform needs.
662
+ *
663
+ * @category constructors
664
+ * @since 0.1.0
665
+ */
666
+ export const systemFor = (platform: string): System =>
667
+ platform === "win32" ? windowsSystem : platform === "linux" ? procSystem : posixSystem
668
+
669
+ /**
670
+ * How far apart a recorded start time and the operating system's may be and
671
+ * still describe the same process.
672
+ *
673
+ * `lstart` has one-second granularity and the ledger writes its record just
674
+ * after the spawn returns, so the two never agree exactly. Two seconds covers
675
+ * the gap; a pid the operating system handed to somebody else is minutes or
676
+ * hours away from it, not milliseconds.
677
+ *
678
+ * @category models
679
+ * @since 0.1.0
680
+ */
681
+ export const identityToleranceMs = 2000
682
+
683
+ /**
684
+ * Reaper configuration.
685
+ *
686
+ * @category models
687
+ * @since 0.1.0
688
+ */
689
+ export interface Options {
690
+ /** The pid this host must never signal a group for. Default `process.pid`. */
691
+ readonly ownerPid?: number | undefined
692
+ /** The operating-system seam. Default {@link systemFor} of `process.platform`. */
693
+ readonly system?: System | undefined
694
+ }
695
+
696
+ /**
697
+ * Why a record was not signalled.
698
+ *
699
+ * @category models
700
+ * @since 0.1.0
701
+ */
702
+ export type Refusal =
703
+ /** The incarnation that started it is still running and will contain it. */
704
+ | "owner-alive"
705
+ /** It shared its owner's process group, so there is no group to signal. */
706
+ | "no-group"
707
+ /** It named this host's own process group or pid. */
708
+ | "own-group"
709
+ /** This host could not read its OWN process group, so it cannot rule that out. */
710
+ | "own-group-unknown"
711
+ /** Its numbers do not name anything this platform may signal. */
712
+ | "invalid-record"
713
+ /** It was written before this machine booted, so the pid space is not the same. */
714
+ | "pre-boot"
715
+ /** The pid it names does not exist, so there is no identity to confirm. */
716
+ | "process-gone"
717
+ /** The pid exists but did not start when the record says it did. */
718
+ | "identity-mismatch"
719
+ /** This host could not ask when the pid started, so it must not signal it. */
720
+ | "identity-unverified"
721
+ /** The signal was refused, so the record stays for the next incarnation. */
722
+ | "kill-failed"
723
+
724
+ /**
725
+ * The refusals that leave a record inherited instead of retiring it.
726
+ *
727
+ * A refusal is final when the operating system has moved on from the number:
728
+ * saying so in the journal is what stops every later incarnation re-examining
729
+ * it. These three are not final. `owner-alive` means the incarnation that owns
730
+ * the record will contain its own children; `identity-unverified` and
731
+ * `own-group-unknown` mean this host could not ask a question at all — a host
732
+ * that CAN ask has to get the chance.
733
+ */
734
+ const retained: ReadonlySet<Refusal> = new Set<Refusal>([
735
+ "owner-alive",
736
+ "identity-unverified",
737
+ "own-group-unknown"
738
+ ])
739
+
740
+ /**
741
+ * One record, and what the reaper decided about it.
742
+ *
743
+ * The two outcomes are a union rather than one shape with an optional field so
744
+ * that `killed` narrows the reason: after `if (!entry.killed)` the
745
+ * {@link Refusal} is there to read, and a killed record cannot be built
746
+ * carrying a reason it never had. The runtime objects are unchanged — a killed
747
+ * entry simply omits `refusal`.
748
+ *
749
+ * @category models
750
+ * @since 0.1.0
751
+ */
752
+ export type Reaped =
753
+ | {
754
+ readonly record: ProcessLedger.ProcessRecord
755
+ readonly killed: true
756
+ /** Never present when `killed` is `true`. */
757
+ readonly refusal?: never
758
+ }
759
+ | {
760
+ readonly record: ProcessLedger.ProcessRecord
761
+ readonly killed: false
762
+ /** Present exactly when `killed` is `false`. */
763
+ readonly refusal: Refusal
764
+ }
765
+
766
+ /** The identity half of the decision, split out because it reads as a list. */
767
+ const refuseOnIdentity = (
768
+ system: System,
769
+ record: ProcessLedger.ProcessRecord,
770
+ bootMs: number
771
+ ): Refusal | undefined => {
772
+ // A record older than this boot names a pid from a pid space that no longer
773
+ // exists. The tolerance points BACKWARDS from the boot instant on purpose:
774
+ // `uptime` is second-granular, so a record written moments after boot could
775
+ // otherwise be read as predating it, and refusing to kill is the safe way to
776
+ // be wrong.
777
+ //
778
+ // The consequence is a narrow, deliberate leak, and it is a leak rather than
779
+ // a retry: `reap` retires a `pre-boot` refusal through `ledger.skipped`, so a
780
+ // group spawned within `identityToleranceMs` of the computed boot instant is
781
+ // never reaped by any later incarnation either. That is the price of refusing
782
+ // to kill on an instant `uptime` can only place to the nearest second.
783
+ if (record.startedAtMs < bootMs + identityToleranceMs) return "pre-boot"
784
+ const started = system.startedAtMs(record.pid)
785
+ // A platform with no start-time column at all has already been checked
786
+ // against this boot, which is the whole of its documented weaker guarantee.
787
+ if (started._tag === "unsupported") return undefined
788
+ if (started._tag === "gone") return "process-gone"
789
+ if (started._tag === "unavailable") return "identity-unverified"
790
+ return Math.abs(started.startedAtMs - record.startedAtMs) <= identityToleranceMs
791
+ ? undefined
792
+ : "identity-mismatch"
793
+ }
794
+
795
+ /** Everything the reaper checks before it is willing to signal a record. */
796
+ const refuse = (
797
+ system: System,
798
+ record: ProcessLedger.ProcessRecord,
799
+ ownerPid: number,
800
+ ownGroup: number | null,
801
+ bootMs: number
802
+ ): Refusal | undefined => {
803
+ // The host's own identity comes first, before the platform is consulted at
804
+ // all: a record naming this process is refused on every platform, whatever
805
+ // shape its numbers have.
806
+ if (record.pgid === ownerPid || record.pid === ownerPid) return "own-group"
807
+ if (ownGroup !== null && (record.pgid === ownGroup || record.pid === ownGroup)) return "own-group"
808
+ const target = system.refuseTarget(record)
809
+ if (target !== undefined) return target
810
+ // A record naming a group can only be cleared of naming THIS host's group by
811
+ // reading that group. When the probe cannot answer, the comparison above was
812
+ // not made rather than made and passed, so the record is kept instead of
813
+ // signalled on a guard that did not run.
814
+ if (record.pgid !== null && ownGroup === null) return "own-group-unknown"
815
+ if (system.isAlive(record.ownerPid) !== "dead") return "owner-alive"
816
+ return refuseOnIdentity(system, record, bootMs)
817
+ }
818
+
819
+ /**
820
+ * Kills the process groups a previous incarnation of this host abandoned.
821
+ *
822
+ * Returns one entry per inherited record so a caller can log what it decided;
823
+ * `killed: false` carries the {@link Refusal} that produced it. Fails with
824
+ * {@link ProcessLedger.ProcessLedgerReplayError} when the inherited history
825
+ * cannot be read to its end: nothing is signalled or retired, so the next
826
+ * sweep inherits every record again.
827
+ *
828
+ * @category constructors
829
+ * @since 0.1.0
830
+ */
831
+ export const reap = (
832
+ options?: Options
833
+ ): Effect.Effect<ReadonlyArray<Reaped>, ProcessLedger.ProcessLedgerReplayError, ProcessLedger.ProcessLedger> =>
834
+ Effect.gen(function*() {
835
+ const ledger = yield* ProcessLedger.ProcessLedger
836
+ const system = options?.system ?? systemFor(process.platform)
837
+ const ownerPid = options?.ownerPid ?? process.pid
838
+ const ownGroup = system.ownGroup()
839
+ const bootMs = system.bootedAtMs()
840
+ const orphans = yield* ledger.orphans
841
+ const decided: Array<Reaped> = []
842
+ for (const record of orphans) {
843
+ const refusal = refuse(system, record, ownerPid, ownGroup, bootMs)
844
+ if (refusal === undefined) {
845
+ const outcome = system.killTree(record)
846
+ if (outcome === "failed") {
847
+ // Nothing was killed, so nothing is retired: leaving the record
848
+ // inherited is what makes the next incarnation try again.
849
+ decided.push({ record, killed: false, refusal: "kill-failed" })
850
+ continue
851
+ }
852
+ yield* retire(ledger.reaped(record), record.pid, "reaped")
853
+ decided.push({ record, killed: true })
854
+ continue
855
+ }
856
+ // A retained refusal is not this host's to retire; every other refusal is
857
+ // final and says so in the journal.
858
+ if (!retained.has(refusal)) {
859
+ yield* retire(ledger.skipped(record, refusal), record.pid, "reap-skipped")
860
+ }
861
+ decided.push({ record, killed: false, refusal })
862
+ }
863
+ return decided
864
+ })
865
+
866
+ /**
867
+ * Writes one ledger retirement, logging rather than failing the sweep.
868
+ *
869
+ * A retirement that does not commit is not a lost kill: the process is already
870
+ * dead and the record stays inherited, so the next incarnation reads it, finds
871
+ * the pid gone, and retires it then. Failing the whole sweep over it would
872
+ * take the host down for a bookkeeping write.
873
+ */
874
+ const retire = (
875
+ write: Effect.Effect<void, unknown>,
876
+ pid: number,
877
+ what: string
878
+ ): Effect.Effect<void> =>
879
+ write.pipe(
880
+ Effect.catch((error) => Effect.logWarning(`process reaper could not journal ${what} for ${pid}`, error))
881
+ )
882
+
883
+ /**
884
+ * Refusals that leave a dead incarnation's process running with no live owner
885
+ * to contain it: the host could not ask, or its signal failed.
886
+ */
887
+ const unreaped: ReadonlySet<Refusal> = new Set<Refusal>(["identity-unverified", "own-group-unknown", "kill-failed"])
888
+
889
+ /**
890
+ * Counter over inherited records the reaper did not kill, with a `refusal`
891
+ * attribute naming why.
892
+ *
893
+ * @category metrics
894
+ * @since 1.0.0-rc.1
895
+ */
896
+ export const refusals = Metric.counter("flows_process_reaper_refusals", {
897
+ description: "Inherited process records the reaper did not kill, by refusal"
898
+ })
899
+
900
+ /**
901
+ * Reports one sweep: a counter update per refusal and one summary line. The
902
+ * line is a warning when a record stays inherited (a retained refusal) or a
903
+ * signal failed, because either leaves a crashed incarnation's processes
904
+ * running with nothing else to say so.
905
+ *
906
+ * @category layers
907
+ * @since 1.0.0-rc.1
908
+ */
909
+ export const report = (decided: ReadonlyArray<Reaped>): Effect.Effect<void> =>
910
+ Effect.gen(function*() {
911
+ if (decided.length === 0) return
912
+ const counts: Partial<Record<Refusal, number>> = {}
913
+ let killed = 0
914
+ for (const entry of decided) {
915
+ if (entry.killed) {
916
+ killed += 1
917
+ continue
918
+ }
919
+ counts[entry.refusal] = (counts[entry.refusal] ?? 0) + 1
920
+ }
921
+ for (const [refusal, count] of Object.entries(counts)) {
922
+ yield* Metric.update(Metric.withAttributes(refusals, { refusal }), count)
923
+ }
924
+ const annotations = { killed, ...counts }
925
+ const alarming = decided.some((entry) => !entry.killed && unreaped.has(entry.refusal))
926
+ const line = "process reaper swept inherited records"
927
+ yield* (alarming ? Effect.logWarning(line) : Effect.logInfo(line)).pipe(Effect.annotateLogs(annotations))
928
+ })
929
+
930
+ /**
931
+ * Runs {@link reap} once while the layer is built.
932
+ *
933
+ * Compose it into a host layer so standing a host up is also what cleans up
934
+ * after the incarnation that crashed. A history that cannot be read refuses
935
+ * the sweep with an error log and leaves every record inherited; the host
936
+ * still starts, because the processes it will spawn are recorded and
937
+ * contained regardless. Every decision is {@link report}ed.
938
+ *
939
+ * @category layers
940
+ * @since 0.1.0
941
+ */
942
+ export const layer = (options?: Options): Layer.Layer<never, never, ProcessLedger.ProcessLedger> =>
943
+ Layer.effectDiscard(
944
+ reap(options).pipe(
945
+ Effect.flatMap(report),
946
+ Effect.catchTag(
947
+ "@smthrs/kernel/ProcessLedgerReplayError",
948
+ (error) => Effect.logError("process reaper refused to sweep: the ledger history is incomplete", error)
949
+ )
950
+ )
951
+ )