@mysten-incubation/devstack 0.1.1 → 0.2.0

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 (858) hide show
  1. package/README.md +6 -3
  2. package/dashboard-ui/assets/grpc-a4usE3Nk.js +3 -0
  3. package/dashboard-ui/assets/index-B82Bh84P.css +1 -0
  4. package/dashboard-ui/assets/index-CRYJ4pru.js +1277 -0
  5. package/dashboard-ui/index.html +2 -2
  6. package/dist/api/define-capabilities.d.mts +1 -20
  7. package/dist/api/define-capabilities.mjs +15 -22
  8. package/dist/api/define-capabilities.mjs.map +1 -1
  9. package/dist/api/define-devstack-with.d.mts +7 -1
  10. package/dist/api/define-devstack-with.mjs.map +1 -1
  11. package/dist/api/define-devstack.mjs.map +1 -1
  12. package/dist/api/define-plugin.d.mts +2 -1
  13. package/dist/api/inference-network.d.mts +31 -17
  14. package/dist/api/inference-network.mjs +40 -43
  15. package/dist/api/inference-network.mjs.map +1 -1
  16. package/dist/api/load-config.mjs +88 -0
  17. package/dist/api/load-config.mjs.map +1 -0
  18. package/dist/api/mode-narrowed-factory.d.mts +1 -1
  19. package/dist/api/mode-narrowed-factory.mjs.map +1 -1
  20. package/dist/api/run-stack-internal.mjs +202 -0
  21. package/dist/api/run-stack-internal.mjs.map +1 -0
  22. package/dist/api/run-stack.d.mts +82 -31
  23. package/dist/api/run-stack.mjs +22 -141
  24. package/dist/api/run-stack.mjs.map +1 -1
  25. package/dist/build-integrations/dapp-kit/index.d.mts +61 -0
  26. package/dist/build-integrations/dapp-kit/index.mjs +58 -0
  27. package/dist/build-integrations/dapp-kit/index.mjs.map +1 -0
  28. package/dist/build-integrations/playwright/config.d.mts +1 -30
  29. package/dist/build-integrations/playwright/config.mjs +1 -27
  30. package/dist/build-integrations/playwright/config.mjs.map +1 -1
  31. package/dist/build-integrations/playwright/errors.d.mts +4 -7
  32. package/dist/build-integrations/playwright/errors.mjs.map +1 -1
  33. package/dist/build-integrations/playwright/global-setup.d.mts +42 -71
  34. package/dist/build-integrations/playwright/global-setup.mjs +142 -218
  35. package/dist/build-integrations/playwright/global-setup.mjs.map +1 -1
  36. package/dist/build-integrations/playwright/index.d.mts +2 -2
  37. package/dist/build-integrations/playwright/index.mjs +2 -2
  38. package/dist/build-integrations/playwright/stack-context.d.mts +3 -1
  39. package/dist/build-integrations/playwright/stack-context.mjs +4 -2
  40. package/dist/build-integrations/playwright/stack-context.mjs.map +1 -1
  41. package/dist/build-integrations/playwright/wallet-context.mjs +44 -19
  42. package/dist/build-integrations/playwright/wallet-context.mjs.map +1 -1
  43. package/dist/build-integrations/runtime/cold-start-url.mjs.map +1 -1
  44. package/dist/build-integrations/runtime/conventional-routes.mjs.map +1 -1
  45. package/dist/build-integrations/runtime/dapp-kit-slot.mjs.map +1 -1
  46. package/dist/build-integrations/runtime/discover.d.mts +6 -4
  47. package/dist/build-integrations/runtime/discover.mjs +15 -4
  48. package/dist/build-integrations/runtime/discover.mjs.map +1 -1
  49. package/dist/build-integrations/runtime/endpoint-registry.mjs.map +1 -1
  50. package/dist/build-integrations/runtime/errors.mjs.map +1 -1
  51. package/dist/build-integrations/runtime/read-stack-context.mjs +3 -3
  52. package/dist/build-integrations/runtime/read-stack-context.mjs.map +1 -1
  53. package/dist/build-integrations/runtime/resolve-discovery-env.d.mts +23 -7
  54. package/dist/build-integrations/runtime/resolve-discovery-env.mjs +25 -12
  55. package/dist/build-integrations/runtime/resolve-discovery-env.mjs.map +1 -1
  56. package/dist/build-integrations/runtime/stack-context.d.mts +2 -2
  57. package/dist/build-integrations/vite/index.d.mts +84 -19
  58. package/dist/build-integrations/vite/index.mjs +203 -28
  59. package/dist/build-integrations/vite/index.mjs.map +1 -1
  60. package/dist/build-integrations/vitest/config.d.mts +20 -2
  61. package/dist/build-integrations/vitest/config.mjs +16 -4
  62. package/dist/build-integrations/vitest/config.mjs.map +1 -1
  63. package/dist/build-integrations/vitest/env.d.mts +18 -10
  64. package/dist/build-integrations/vitest/env.mjs +11 -10
  65. package/dist/build-integrations/vitest/env.mjs.map +1 -1
  66. package/dist/build-integrations/vitest/errors.d.mts +7 -11
  67. package/dist/build-integrations/vitest/errors.mjs +3 -4
  68. package/dist/build-integrations/vitest/errors.mjs.map +1 -1
  69. package/dist/build-integrations/vitest/global-setup.d.mts +35 -0
  70. package/dist/build-integrations/vitest/global-setup.mjs +156 -0
  71. package/dist/build-integrations/vitest/global-setup.mjs.map +1 -0
  72. package/dist/build-integrations/vitest/index.d.mts +3 -2
  73. package/dist/build-integrations/vitest/index.mjs +3 -2
  74. package/dist/build-integrations/vitest/setup.mjs +2 -2
  75. package/dist/build-integrations/vitest/setup.mjs.map +1 -1
  76. package/dist/build-integrations/vitest/stack-context.d.mts +5 -3
  77. package/dist/build-integrations/vitest/stack-context.mjs +3 -2
  78. package/dist/build-integrations/vitest/stack-context.mjs.map +1 -1
  79. package/dist/cli/bail.mjs.map +1 -1
  80. package/dist/cli/doctor-probes.mjs +1 -3
  81. package/dist/cli/doctor-probes.mjs.map +1 -1
  82. package/dist/cli/main.d.mts +23 -1
  83. package/dist/cli/main.mjs +63 -8
  84. package/dist/cli/main.mjs.map +1 -1
  85. package/dist/cli/prune-direct.mjs.map +1 -1
  86. package/dist/cli/snapshot-reader.mjs.map +1 -1
  87. package/dist/cli/up-lifecycle.mjs.map +1 -1
  88. package/dist/cli/wirings/apply.mjs +28 -21
  89. package/dist/cli/wirings/apply.mjs.map +1 -1
  90. package/dist/cli/wirings/codegen.mjs +108 -0
  91. package/dist/cli/wirings/codegen.mjs.map +1 -0
  92. package/dist/cli/wirings/config-loader.mjs +12 -62
  93. package/dist/cli/wirings/config-loader.mjs.map +1 -1
  94. package/dist/cli/wirings/dump-ids.mjs +86 -0
  95. package/dist/cli/wirings/dump-ids.mjs.map +1 -0
  96. package/dist/cli/wirings/engine-command.mjs +1 -1
  97. package/dist/cli/wirings/engine-command.mjs.map +1 -1
  98. package/dist/cli/wirings/identity.mjs +2 -2
  99. package/dist/cli/wirings/identity.mjs.map +1 -1
  100. package/dist/cli/wirings/provide-file-system.mjs.map +1 -1
  101. package/dist/cli/wirings/read-devstack-version.mjs +17 -0
  102. package/dist/cli/wirings/read-devstack-version.mjs.map +1 -0
  103. package/dist/cli/wirings/snapshot.mjs +27 -34
  104. package/dist/cli/wirings/snapshot.mjs.map +1 -1
  105. package/dist/cli/wirings/up-ipc.mjs +147 -0
  106. package/dist/cli/wirings/up-ipc.mjs.map +1 -0
  107. package/dist/cli/wirings/up.mjs +218 -198
  108. package/dist/cli/wirings/up.mjs.map +1 -1
  109. package/dist/cli/wirings/wipe.mjs +1 -1
  110. package/dist/cli/wirings/wipe.mjs.map +1 -1
  111. package/dist/contracts/chain-probe.d.mts +1 -1
  112. package/dist/contracts/chain-probe.mjs +1 -1
  113. package/dist/contracts/chain-probe.mjs.map +1 -1
  114. package/dist/contracts/codegenable.d.mts +60 -6
  115. package/dist/contracts/codegenable.mjs +28 -0
  116. package/dist/contracts/codegenable.mjs.map +1 -0
  117. package/dist/contracts/config-bindings.mjs +162 -0
  118. package/dist/contracts/config-bindings.mjs.map +1 -0
  119. package/dist/contracts/container-runtime.d.mts +23 -17
  120. package/dist/contracts/plugin-expander.mjs.map +1 -1
  121. package/dist/contracts/snapshotable.d.mts +5 -7
  122. package/dist/contracts/wallet-protocol.d.mts +2 -5
  123. package/dist/contracts/wallet-protocol.mjs +2 -5
  124. package/dist/contracts/wallet-protocol.mjs.map +1 -1
  125. package/dist/index.d.mts +21 -26
  126. package/dist/index.mjs +8 -9
  127. package/dist/orchestrators/boot.d.mts +28 -0
  128. package/dist/orchestrators/boot.mjs +440 -0
  129. package/dist/orchestrators/boot.mjs.map +1 -0
  130. package/dist/orchestrators/codegen/bindings.d.mts +13 -1
  131. package/dist/orchestrators/codegen/bindings.mjs +23 -9
  132. package/dist/orchestrators/codegen/bindings.mjs.map +1 -1
  133. package/dist/orchestrators/codegen/config-runtime.mjs +208 -0
  134. package/dist/orchestrators/codegen/config-runtime.mjs.map +1 -0
  135. package/dist/orchestrators/codegen/emit.mjs +1 -1
  136. package/dist/orchestrators/codegen/emit.mjs.map +1 -1
  137. package/dist/orchestrators/codegen/errors.mjs +21 -1
  138. package/dist/orchestrators/codegen/errors.mjs.map +1 -1
  139. package/dist/orchestrators/codegen/format.mjs +2 -0
  140. package/dist/orchestrators/codegen/format.mjs.map +1 -1
  141. package/dist/orchestrators/codegen/gitignore.mjs +30 -11
  142. package/dist/orchestrators/codegen/gitignore.mjs.map +1 -1
  143. package/dist/orchestrators/codegen/id-config.mjs +107 -0
  144. package/dist/orchestrators/codegen/id-config.mjs.map +1 -0
  145. package/dist/orchestrators/codegen/output-location.mjs +23 -32
  146. package/dist/orchestrators/codegen/output-location.mjs.map +1 -1
  147. package/dist/orchestrators/codegen/paths.mjs +8 -1
  148. package/dist/orchestrators/codegen/paths.mjs.map +1 -1
  149. package/dist/orchestrators/codegen/permissions.mjs.map +1 -1
  150. package/dist/orchestrators/codegen/service.mjs +286 -58
  151. package/dist/orchestrators/codegen/service.mjs.map +1 -1
  152. package/dist/orchestrators/layers.mjs +31 -0
  153. package/dist/orchestrators/layers.mjs.map +1 -0
  154. package/dist/orchestrators/lifecycle-prune/errors.mjs.map +1 -1
  155. package/dist/orchestrators/lifecycle-prune/index.mjs +8 -10
  156. package/dist/orchestrators/lifecycle-prune/index.mjs.map +1 -1
  157. package/dist/orchestrators/network-options.d.mts +52 -0
  158. package/dist/orchestrators/network-options.mjs +43 -0
  159. package/dist/orchestrators/network-options.mjs.map +1 -0
  160. package/dist/orchestrators/router/cleanup.mjs.map +1 -1
  161. package/dist/orchestrators/router/cors.mjs.map +1 -1
  162. package/dist/orchestrators/router/entrypoints.mjs.map +1 -1
  163. package/dist/orchestrators/router/errors.mjs.map +1 -1
  164. package/dist/orchestrators/router/file-provider.mjs.map +1 -1
  165. package/dist/orchestrators/router/hostname.mjs.map +1 -1
  166. package/dist/orchestrators/router/profile.mjs +1 -1
  167. package/dist/orchestrators/router/profile.mjs.map +1 -1
  168. package/dist/orchestrators/router/service.d.mts +2 -2
  169. package/dist/orchestrators/router/service.mjs +12 -27
  170. package/dist/orchestrators/router/service.mjs.map +1 -1
  171. package/dist/orchestrators/router/traefik-container.mjs +1 -1
  172. package/dist/orchestrators/router/traefik-container.mjs.map +1 -1
  173. package/dist/orchestrators/snapshot/capture.mjs +171 -195
  174. package/dist/orchestrators/snapshot/capture.mjs.map +1 -1
  175. package/dist/orchestrators/snapshot/descriptor.d.mts +1 -1
  176. package/dist/orchestrators/snapshot/descriptor.mjs +53 -11
  177. package/dist/orchestrators/snapshot/descriptor.mjs.map +1 -1
  178. package/dist/orchestrators/snapshot/identity-guard.mjs +3 -5
  179. package/dist/orchestrators/snapshot/identity-guard.mjs.map +1 -1
  180. package/dist/orchestrators/snapshot/image-bundle-tags.mjs +32 -101
  181. package/dist/orchestrators/snapshot/image-bundle-tags.mjs.map +1 -1
  182. package/dist/orchestrators/snapshot/index.mjs +1 -4
  183. package/dist/orchestrators/snapshot/integrity.mjs.map +1 -1
  184. package/dist/orchestrators/snapshot/interrupted-restore.mjs +116 -0
  185. package/dist/orchestrators/snapshot/interrupted-restore.mjs.map +1 -0
  186. package/dist/orchestrators/snapshot/phase-error.mjs.map +1 -1
  187. package/dist/orchestrators/snapshot/prune.mjs +40 -31
  188. package/dist/orchestrators/snapshot/prune.mjs.map +1 -1
  189. package/dist/orchestrators/snapshot/restore.mjs +221 -196
  190. package/dist/orchestrators/snapshot/restore.mjs.map +1 -1
  191. package/dist/orchestrators/snapshot/service.mjs +72 -62
  192. package/dist/orchestrators/snapshot/service.mjs.map +1 -1
  193. package/dist/orchestrators/snapshot/wipe.mjs +67 -56
  194. package/dist/orchestrators/snapshot/wipe.mjs.map +1 -1
  195. package/dist/plugins/account/codegen.mjs +3 -0
  196. package/dist/plugins/account/codegen.mjs.map +1 -1
  197. package/dist/plugins/account/errors.d.mts +2 -2
  198. package/dist/plugins/account/errors.mjs +1 -4
  199. package/dist/plugins/account/errors.mjs.map +1 -1
  200. package/dist/plugins/account/funding.mjs +3 -27
  201. package/dist/plugins/account/funding.mjs.map +1 -1
  202. package/dist/plugins/account/index.d.mts +3 -8
  203. package/dist/plugins/account/index.mjs +71 -35
  204. package/dist/plugins/account/index.mjs.map +1 -1
  205. package/dist/plugins/account/keypair.d.mts +8 -7
  206. package/dist/plugins/account/keypair.mjs +5 -18
  207. package/dist/plugins/account/keypair.mjs.map +1 -1
  208. package/dist/plugins/account/lease.mjs.map +1 -1
  209. package/dist/plugins/account/registry.d.mts +1 -46
  210. package/dist/plugins/account/registry.mjs.map +1 -1
  211. package/dist/plugins/account/service.d.mts +43 -34
  212. package/dist/plugins/account/service.mjs +9 -54
  213. package/dist/plugins/account/service.mjs.map +1 -1
  214. package/dist/plugins/account/snapshot.mjs.map +1 -1
  215. package/dist/plugins/account/variants/ephemeral.mjs.map +1 -1
  216. package/dist/plugins/account/variants/impersonate.mjs.map +1 -1
  217. package/dist/plugins/account/variants/signer.mjs.map +1 -1
  218. package/dist/plugins/action/discriminator.mjs.map +1 -1
  219. package/dist/plugins/action/errors.mjs +1 -4
  220. package/dist/plugins/action/errors.mjs.map +1 -1
  221. package/dist/plugins/action/execute.mjs +3 -4
  222. package/dist/plugins/action/execute.mjs.map +1 -1
  223. package/dist/plugins/action/index.d.mts +2 -2
  224. package/dist/plugins/action/index.mjs +7 -11
  225. package/dist/plugins/action/index.mjs.map +1 -1
  226. package/dist/plugins/action/service.d.mts +5 -7
  227. package/dist/plugins/action/service.mjs +2 -16
  228. package/dist/plugins/action/service.mjs.map +1 -1
  229. package/dist/plugins/coin/address-resolution.mjs.map +1 -1
  230. package/dist/plugins/coin/codegen.mjs +97 -16
  231. package/dist/plugins/coin/codegen.mjs.map +1 -1
  232. package/dist/plugins/coin/discovery.mjs.map +1 -1
  233. package/dist/plugins/coin/errors.mjs +1 -4
  234. package/dist/plugins/coin/errors.mjs.map +1 -1
  235. package/dist/plugins/coin/index.d.mts +4 -34
  236. package/dist/plugins/coin/index.mjs +47 -28
  237. package/dist/plugins/coin/index.mjs.map +1 -1
  238. package/dist/plugins/coin/metadata.d.mts +1 -0
  239. package/dist/plugins/coin/metadata.mjs +8 -9
  240. package/dist/plugins/coin/metadata.mjs.map +1 -1
  241. package/dist/plugins/coin/mint.mjs +7 -21
  242. package/dist/plugins/coin/mint.mjs.map +1 -1
  243. package/dist/plugins/coin/registry.mjs +33 -20
  244. package/dist/plugins/coin/registry.mjs.map +1 -1
  245. package/dist/plugins/coin/service.d.mts +1 -1
  246. package/dist/plugins/coin/service.mjs +1 -8
  247. package/dist/plugins/coin/service.mjs.map +1 -1
  248. package/dist/plugins/coin/snapshot.mjs.map +1 -1
  249. package/dist/plugins/coin/type-strings.mjs.map +1 -1
  250. package/dist/plugins/dashboard/domain.mjs +106 -167
  251. package/dist/plugins/dashboard/domain.mjs.map +1 -1
  252. package/dist/plugins/dashboard/index.mjs +13 -13
  253. package/dist/plugins/dashboard/index.mjs.map +1 -1
  254. package/dist/plugins/dashboard/origin-policy.mjs +3 -3
  255. package/dist/plugins/dashboard/origin-policy.mjs.map +1 -1
  256. package/dist/plugins/dashboard/routable.mjs.map +1 -1
  257. package/dist/plugins/dashboard/schema/builder.mjs.map +1 -1
  258. package/dist/plugins/dashboard/schema/enums.mjs.map +1 -1
  259. package/dist/plugins/dashboard/schema/root.mjs +23 -44
  260. package/dist/plugins/dashboard/schema/root.mjs.map +1 -1
  261. package/dist/plugins/dashboard/schema/types.mjs +37 -91
  262. package/dist/plugins/dashboard/schema/types.mjs.map +1 -1
  263. package/dist/plugins/dashboard/schema.mjs.map +1 -1
  264. package/dist/plugins/dashboard/server.mjs.map +1 -1
  265. package/dist/plugins/deepbook/codegen.d.mts +12 -11
  266. package/dist/plugins/deepbook/codegen.mjs +108 -11
  267. package/dist/plugins/deepbook/codegen.mjs.map +1 -1
  268. package/dist/plugins/deepbook/deploy.mjs +7 -14
  269. package/dist/plugins/deepbook/deploy.mjs.map +1 -1
  270. package/dist/plugins/deepbook/errors.mjs +4 -2
  271. package/dist/plugins/deepbook/errors.mjs.map +1 -1
  272. package/dist/plugins/deepbook/faucet-strategy.mjs +4 -10
  273. package/dist/plugins/deepbook/faucet-strategy.mjs.map +1 -1
  274. package/dist/plugins/deepbook/hash.mjs.map +1 -1
  275. package/dist/plugins/deepbook/index.d.mts +38 -34
  276. package/dist/plugins/deepbook/index.mjs +133 -64
  277. package/dist/plugins/deepbook/index.mjs.map +1 -1
  278. package/dist/plugins/deepbook/plugin-key.mjs.map +1 -1
  279. package/dist/plugins/deepbook/pyth/index.mjs +4 -8
  280. package/dist/plugins/deepbook/pyth/index.mjs.map +1 -1
  281. package/dist/plugins/deepbook/snapshot.mjs +0 -1
  282. package/dist/plugins/deepbook/snapshot.mjs.map +1 -1
  283. package/dist/plugins/deepbook/type-strings.mjs.map +1 -1
  284. package/dist/plugins/deepbook/types.d.mts +2 -1
  285. package/dist/plugins/deepbook/types.mjs.map +1 -1
  286. package/dist/plugins/faucet/dispatcher.d.mts +3 -3
  287. package/dist/plugins/faucet/dispatcher.mjs.map +1 -1
  288. package/dist/plugins/faucet/errors.mjs.map +1 -1
  289. package/dist/plugins/faucet/http.mjs +2 -10
  290. package/dist/plugins/faucet/http.mjs.map +1 -1
  291. package/dist/plugins/faucet/index.mjs +0 -1
  292. package/dist/plugins/faucet/index.mjs.map +1 -1
  293. package/dist/plugins/host-service/errors.d.mts +1 -3
  294. package/dist/plugins/host-service/errors.mjs +1 -2
  295. package/dist/plugins/host-service/errors.mjs.map +1 -1
  296. package/dist/plugins/host-service/index.d.mts +1 -2
  297. package/dist/plugins/host-service/index.mjs +11 -11
  298. package/dist/plugins/host-service/index.mjs.map +1 -1
  299. package/dist/plugins/host-service/routable.mjs.map +1 -1
  300. package/dist/plugins/host-service/service.mjs +13 -13
  301. package/dist/plugins/host-service/service.mjs.map +1 -1
  302. package/dist/plugins/internal/acquire-on-chain-artifact.mjs.map +1 -1
  303. package/dist/plugins/internal/codegen-helpers.mjs +2 -0
  304. package/dist/plugins/internal/codegen-helpers.mjs.map +1 -1
  305. package/dist/plugins/internal/funding-failure-error.mjs.map +1 -1
  306. package/dist/plugins/{postgres → internal/postgres-sidecar}/connection.mjs +1 -1
  307. package/dist/plugins/internal/postgres-sidecar/connection.mjs.map +1 -0
  308. package/dist/plugins/{postgres → internal/postgres-sidecar}/db-ensure.mjs +30 -16
  309. package/dist/plugins/internal/postgres-sidecar/db-ensure.mjs.map +1 -0
  310. package/dist/plugins/internal/postgres-sidecar/errors.mjs +21 -0
  311. package/dist/plugins/internal/postgres-sidecar/errors.mjs.map +1 -0
  312. package/dist/{substrate/runtime/scoped-multimap → plugins/internal/postgres-sidecar}/index.mjs +1 -0
  313. package/dist/plugins/internal/postgres-sidecar/service.mjs +121 -0
  314. package/dist/plugins/internal/postgres-sidecar/service.mjs.map +1 -0
  315. package/dist/plugins/package/build.mjs +2 -2
  316. package/dist/plugins/package/build.mjs.map +1 -1
  317. package/dist/plugins/package/codegen.d.mts +7 -14
  318. package/dist/plugins/package/codegen.mjs +187 -59
  319. package/dist/plugins/package/codegen.mjs.map +1 -1
  320. package/dist/plugins/package/dep-resolution.mjs +51 -3
  321. package/dist/plugins/package/dep-resolution.mjs.map +1 -1
  322. package/dist/plugins/package/errors.mjs +1 -4
  323. package/dist/plugins/package/errors.mjs.map +1 -1
  324. package/dist/plugins/package/git-source.d.mts +18 -0
  325. package/dist/plugins/package/git-source.mjs +119 -0
  326. package/dist/plugins/package/git-source.mjs.map +1 -0
  327. package/dist/plugins/package/index.d.mts +28 -19
  328. package/dist/plugins/package/index.mjs +107 -32
  329. package/dist/plugins/package/index.mjs.map +1 -1
  330. package/dist/plugins/package/mode-known.mjs +2 -2
  331. package/dist/plugins/package/mode-known.mjs.map +1 -1
  332. package/dist/plugins/package/mode-local.mjs +18 -35
  333. package/dist/plugins/package/mode-local.mjs.map +1 -1
  334. package/dist/plugins/package/publish-executor.mjs +13 -8
  335. package/dist/plugins/package/publish-executor.mjs.map +1 -1
  336. package/dist/plugins/package/publish-output.d.mts +1 -11
  337. package/dist/plugins/package/publish-output.mjs +1 -9
  338. package/dist/plugins/package/publish-output.mjs.map +1 -1
  339. package/dist/plugins/package/registry.d.mts +1 -1
  340. package/dist/plugins/package/registry.mjs +26 -14
  341. package/dist/plugins/package/registry.mjs.map +1 -1
  342. package/dist/plugins/package/service.mjs.map +1 -1
  343. package/dist/plugins/package/snapshot.mjs.map +1 -1
  344. package/dist/plugins/router-entrypoints.mjs +1 -3
  345. package/dist/plugins/router-entrypoints.mjs.map +1 -1
  346. package/dist/plugins/seal/bootstrap-assets/cargo-image.mjs +1 -2
  347. package/dist/plugins/seal/bootstrap-assets/cargo-image.mjs.map +1 -1
  348. package/dist/plugins/seal/bootstrap-assets/source-fetch.mjs +1 -6
  349. package/dist/plugins/seal/bootstrap-assets/source-fetch.mjs.map +1 -1
  350. package/dist/plugins/seal/codegen.d.mts +1 -5
  351. package/dist/plugins/seal/codegen.mjs +83 -12
  352. package/dist/plugins/seal/codegen.mjs.map +1 -1
  353. package/dist/plugins/seal/config-render.mjs +2 -6
  354. package/dist/plugins/seal/config-render.mjs.map +1 -1
  355. package/dist/plugins/seal/deploy.mjs +18 -29
  356. package/dist/plugins/seal/deploy.mjs.map +1 -1
  357. package/dist/plugins/seal/errors.d.mts +4 -5
  358. package/dist/plugins/seal/errors.mjs +1 -8
  359. package/dist/plugins/seal/errors.mjs.map +1 -1
  360. package/dist/plugins/seal/index.d.mts +7 -28
  361. package/dist/plugins/seal/index.mjs +69 -47
  362. package/dist/plugins/seal/index.mjs.map +1 -1
  363. package/dist/plugins/seal/key-manager.mjs.map +1 -1
  364. package/dist/plugins/seal/key-server.mjs +1 -6
  365. package/dist/plugins/seal/key-server.mjs.map +1 -1
  366. package/dist/plugins/seal/keygen.mjs +2 -3
  367. package/dist/plugins/seal/keygen.mjs.map +1 -1
  368. package/dist/plugins/seal/mode/fork-known.mjs.map +1 -1
  369. package/dist/plugins/seal/mode/live.mjs +1 -1
  370. package/dist/plugins/seal/mode/live.mjs.map +1 -1
  371. package/dist/plugins/seal/mode/local-keygen.mjs +4 -10
  372. package/dist/plugins/seal/mode/local-keygen.mjs.map +1 -1
  373. package/dist/plugins/seal/plugin-key.mjs.map +1 -1
  374. package/dist/plugins/seal/registry-publish.mjs.map +1 -1
  375. package/dist/plugins/seal/routable.mjs.map +1 -1
  376. package/dist/plugins/seal/service.mjs.map +1 -1
  377. package/dist/plugins/seal/snapshot.mjs +1 -2
  378. package/dist/plugins/seal/snapshot.mjs.map +1 -1
  379. package/dist/plugins/sui/auto-tick.mjs +5 -5
  380. package/dist/plugins/sui/auto-tick.mjs.map +1 -1
  381. package/dist/plugins/sui/chain-build-container.mjs +2 -2
  382. package/dist/plugins/sui/chain-build-container.mjs.map +1 -1
  383. package/dist/plugins/sui/chain-probe.mjs +4 -4
  384. package/dist/plugins/sui/chain-probe.mjs.map +1 -1
  385. package/dist/plugins/sui/codegen.d.mts +9 -7
  386. package/dist/plugins/sui/codegen.mjs +49 -46
  387. package/dist/plugins/sui/codegen.mjs.map +1 -1
  388. package/dist/plugins/sui/errors.d.mts +1 -1
  389. package/dist/plugins/sui/errors.mjs +1 -9
  390. package/dist/plugins/sui/errors.mjs.map +1 -1
  391. package/dist/{substrate/runtime/sui-execute → plugins/sui/exec}/index.d.mts +1 -1
  392. package/dist/{substrate/runtime/sui-execute → plugins/sui/exec}/index.mjs +3 -7
  393. package/dist/plugins/sui/exec/index.mjs.map +1 -0
  394. package/dist/{substrate/runtime/sui-execute → plugins/sui/exec}/sign-and-dispatch.mjs +1 -1
  395. package/dist/plugins/sui/exec/sign-and-dispatch.mjs.map +1 -0
  396. package/dist/plugins/sui/fork-faucet-strategy.mjs +2 -6
  397. package/dist/plugins/sui/fork-faucet-strategy.mjs.map +1 -1
  398. package/dist/plugins/sui/fork-orchestration.mjs +13 -23
  399. package/dist/plugins/sui/fork-orchestration.mjs.map +1 -1
  400. package/dist/plugins/sui/fork-transaction.mjs.map +1 -1
  401. package/dist/plugins/sui/index.d.mts +34 -83
  402. package/dist/plugins/sui/index.mjs +200 -68
  403. package/dist/plugins/sui/index.mjs.map +1 -1
  404. package/dist/{substrate/runtime/sui-ledger → plugins/sui/ledger}/object-ref.mjs +1 -1
  405. package/dist/plugins/sui/ledger/object-ref.mjs.map +1 -0
  406. package/dist/plugins/sui/local-faucet-strategy.mjs +1 -5
  407. package/dist/plugins/sui/local-faucet-strategy.mjs.map +1 -1
  408. package/dist/plugins/sui/log-attrs.mjs +11 -0
  409. package/dist/plugins/sui/log-attrs.mjs.map +1 -0
  410. package/dist/plugins/sui/mode/external.mjs +4 -12
  411. package/dist/plugins/sui/mode/external.mjs.map +1 -1
  412. package/dist/plugins/sui/mode/fork.mjs +19 -16
  413. package/dist/plugins/sui/mode/fork.mjs.map +1 -1
  414. package/dist/plugins/sui/mode/live.mjs +4 -17
  415. package/dist/plugins/sui/mode/live.mjs.map +1 -1
  416. package/dist/plugins/sui/mode/local.d.mts +1 -0
  417. package/dist/plugins/sui/mode/local.mjs +203 -61
  418. package/dist/plugins/sui/mode/local.mjs.map +1 -1
  419. package/dist/plugins/sui/mode/shared-boot.mjs +18 -23
  420. package/dist/plugins/sui/mode/shared-boot.mjs.map +1 -1
  421. package/dist/plugins/sui/mode/shared.d.mts +6 -7
  422. package/dist/plugins/sui/mode/shared.mjs.map +1 -1
  423. package/dist/plugins/sui/mode/spec.d.mts +23 -6
  424. package/dist/{substrate/runtime/sui-move-build → plugins/sui/move}/index.mjs +28 -30
  425. package/dist/plugins/sui/move/index.mjs.map +1 -0
  426. package/dist/plugins/sui/move-summary-runner.mjs +8 -4
  427. package/dist/plugins/sui/move-summary-runner.mjs.map +1 -1
  428. package/dist/{substrate/network.d.mts → plugins/sui/network-config.d.mts} +11 -10
  429. package/dist/plugins/sui/network-resolver.d.mts +6 -6
  430. package/dist/plugins/sui/routable.mjs.map +1 -1
  431. package/dist/plugins/sui/service.mjs +10 -6
  432. package/dist/plugins/sui/service.mjs.map +1 -1
  433. package/dist/plugins/sui/snapshot.mjs +10 -5
  434. package/dist/plugins/sui/snapshot.mjs.map +1 -1
  435. package/dist/plugins/wallet/codegen.d.mts +8 -6
  436. package/dist/plugins/wallet/codegen.mjs +4 -10
  437. package/dist/plugins/wallet/codegen.mjs.map +1 -1
  438. package/dist/plugins/wallet/errors.mjs +1 -4
  439. package/dist/plugins/wallet/errors.mjs.map +1 -1
  440. package/dist/plugins/wallet/index.d.mts +3 -6
  441. package/dist/plugins/wallet/index.mjs +16 -21
  442. package/dist/plugins/wallet/index.mjs.map +1 -1
  443. package/dist/plugins/wallet/origin-policy.mjs +3 -3
  444. package/dist/plugins/wallet/origin-policy.mjs.map +1 -1
  445. package/dist/plugins/wallet/pairing.mjs +6 -24
  446. package/dist/plugins/wallet/pairing.mjs.map +1 -1
  447. package/dist/plugins/wallet/protocol.mjs.map +1 -1
  448. package/dist/plugins/wallet/routable.mjs.map +1 -1
  449. package/dist/plugins/wallet/server.mjs +17 -24
  450. package/dist/plugins/wallet/server.mjs.map +1 -1
  451. package/dist/plugins/wallet/service.d.mts +2 -2
  452. package/dist/plugins/wallet/service.mjs +4 -15
  453. package/dist/plugins/wallet/service.mjs.map +1 -1
  454. package/dist/plugins/wallet/snapshot.mjs.map +1 -1
  455. package/dist/plugins/walrus/bootstrap-assets/cargo-image.mjs +1 -5
  456. package/dist/plugins/walrus/bootstrap-assets/cargo-image.mjs.map +1 -1
  457. package/dist/plugins/walrus/codegen.d.mts +2 -13
  458. package/dist/plugins/walrus/codegen.mjs +93 -28
  459. package/dist/plugins/walrus/codegen.mjs.map +1 -1
  460. package/dist/plugins/walrus/deploy-paths.mjs +2 -3
  461. package/dist/plugins/walrus/deploy-paths.mjs.map +1 -1
  462. package/dist/plugins/walrus/deploy.mjs +26 -19
  463. package/dist/plugins/walrus/deploy.mjs.map +1 -1
  464. package/dist/plugins/walrus/errors.mjs +1 -8
  465. package/dist/plugins/walrus/errors.mjs.map +1 -1
  466. package/dist/plugins/walrus/faucet-strategy.mjs +15 -1
  467. package/dist/plugins/walrus/faucet-strategy.mjs.map +1 -1
  468. package/dist/plugins/walrus/index.d.mts +13 -44
  469. package/dist/plugins/walrus/index.mjs +119 -122
  470. package/dist/plugins/walrus/index.mjs.map +1 -1
  471. package/dist/plugins/walrus/mode/known-deploy.mjs +6 -6
  472. package/dist/plugins/walrus/mode/known-deploy.mjs.map +1 -1
  473. package/dist/plugins/walrus/mode/local-cluster.d.mts +2 -2
  474. package/dist/plugins/walrus/mode/local-cluster.mjs +2 -10
  475. package/dist/plugins/walrus/mode/local-cluster.mjs.map +1 -1
  476. package/dist/plugins/walrus/plugin-key.mjs.map +1 -1
  477. package/dist/plugins/walrus/registry-publish.mjs.map +1 -1
  478. package/dist/plugins/walrus/routable.mjs.map +1 -1
  479. package/dist/plugins/walrus/service.mjs.map +1 -1
  480. package/dist/plugins/walrus/snapshot.mjs +3 -3
  481. package/dist/plugins/walrus/snapshot.mjs.map +1 -1
  482. package/dist/plugins/walrus/storage-nodes.mjs +53 -14
  483. package/dist/plugins/walrus/storage-nodes.mjs.map +1 -1
  484. package/dist/plugins/walrus/wal-swap.mjs +4 -8
  485. package/dist/plugins/walrus/wal-swap.mjs.map +1 -1
  486. package/dist/primitives/artifact-publisher.mjs.map +1 -1
  487. package/dist/primitives/cache.d.mts +1 -1
  488. package/dist/runtime/docker/client.mjs +47 -3
  489. package/dist/runtime/docker/client.mjs.map +1 -1
  490. package/dist/runtime/docker/container.mjs +37 -57
  491. package/dist/runtime/docker/container.mjs.map +1 -1
  492. package/dist/runtime/docker/errors.mjs.map +1 -1
  493. package/dist/runtime/docker/exec.mjs +2 -2
  494. package/dist/runtime/docker/exec.mjs.map +1 -1
  495. package/dist/runtime/docker/image.mjs +10 -11
  496. package/dist/runtime/docker/image.mjs.map +1 -1
  497. package/dist/runtime/docker/index.mjs +0 -1
  498. package/dist/runtime/docker/inspect-and-decode.mjs.map +1 -1
  499. package/dist/runtime/docker/inventory.mjs +9 -9
  500. package/dist/runtime/docker/inventory.mjs.map +1 -1
  501. package/dist/runtime/docker/labels.mjs +12 -1
  502. package/dist/runtime/docker/labels.mjs.map +1 -1
  503. package/dist/runtime/docker/network.mjs +6 -6
  504. package/dist/runtime/docker/network.mjs.map +1 -1
  505. package/dist/runtime/docker/render-run-args.mjs.map +1 -1
  506. package/dist/runtime/docker/service.mjs +31 -45
  507. package/dist/runtime/docker/service.mjs.map +1 -1
  508. package/dist/runtime/docker/sweep.mjs +14 -64
  509. package/dist/runtime/docker/sweep.mjs.map +1 -1
  510. package/dist/runtime/docker/volume.mjs +1 -1
  511. package/dist/runtime/docker/volume.mjs.map +1 -1
  512. package/dist/runtime/docker/wrap.mjs +14 -2
  513. package/dist/runtime/docker/wrap.mjs.map +1 -1
  514. package/dist/substrate/brand.d.mts +2 -5
  515. package/dist/substrate/brand.mjs +3 -2
  516. package/dist/substrate/brand.mjs.map +1 -1
  517. package/dist/substrate/cross-process.mjs +2 -16
  518. package/dist/substrate/cross-process.mjs.map +1 -1
  519. package/dist/substrate/event-time.mjs +0 -2
  520. package/dist/substrate/event-time.mjs.map +1 -1
  521. package/dist/substrate/events.d.mts +46 -16
  522. package/dist/substrate/identity.d.mts +11 -5
  523. package/dist/substrate/manifest.d.mts +33 -26
  524. package/dist/substrate/manifest.mjs +8 -3
  525. package/dist/substrate/manifest.mjs.map +1 -1
  526. package/dist/substrate/options.d.mts +33 -4
  527. package/dist/substrate/plugin-ctx.d.mts +70 -0
  528. package/dist/substrate/plugin-ctx.mjs +66 -0
  529. package/dist/substrate/plugin-ctx.mjs.map +1 -0
  530. package/dist/substrate/plugin.d.mts +46 -38
  531. package/dist/substrate/plugin.mjs +13 -9
  532. package/dist/substrate/plugin.mjs.map +1 -1
  533. package/dist/substrate/projection.d.mts +1 -1
  534. package/dist/substrate/runtime/atomic-write.mjs +3 -4
  535. package/dist/substrate/runtime/atomic-write.mjs.map +1 -1
  536. package/dist/substrate/runtime/cache/schema.mjs.map +1 -1
  537. package/dist/substrate/runtime/cache/service.mjs +50 -20
  538. package/dist/substrate/runtime/cache/service.mjs.map +1 -1
  539. package/dist/substrate/runtime/config-validation.mjs.map +1 -1
  540. package/dist/substrate/runtime/control-plane/domain.mjs +34 -12
  541. package/dist/substrate/runtime/control-plane/domain.mjs.map +1 -1
  542. package/dist/substrate/runtime/control-plane/service.mjs.map +1 -1
  543. package/dist/substrate/runtime/cross-process/command-channel/channel.mjs +14 -17
  544. package/dist/substrate/runtime/cross-process/command-channel/channel.mjs.map +1 -1
  545. package/dist/substrate/runtime/cross-process/command-channel/file-channel.mjs.map +1 -1
  546. package/dist/substrate/runtime/cross-process/command-channel/protocol.mjs.map +1 -1
  547. package/dist/substrate/runtime/cross-process/command-channel/runtime-control-lock.mjs.map +1 -1
  548. package/dist/substrate/runtime/cross-process/index.mjs +0 -2
  549. package/dist/substrate/runtime/cross-process/live-clock.mjs +1 -1
  550. package/dist/substrate/runtime/cross-process/live-clock.mjs.map +1 -1
  551. package/dist/substrate/runtime/cross-process/liveness.mjs +11 -18
  552. package/dist/substrate/runtime/cross-process/liveness.mjs.map +1 -1
  553. package/dist/substrate/runtime/cross-process/reclaim-stale-file.mjs +2 -2
  554. package/dist/substrate/runtime/cross-process/reclaim-stale-file.mjs.map +1 -1
  555. package/dist/substrate/runtime/cross-process/roster.mjs +17 -127
  556. package/dist/substrate/runtime/cross-process/roster.mjs.map +1 -1
  557. package/dist/substrate/runtime/cross-process/self-pid.mjs.map +1 -1
  558. package/dist/substrate/runtime/cross-process/stack-lock.mjs +29 -28
  559. package/dist/substrate/runtime/cross-process/stack-lock.mjs.map +1 -1
  560. package/dist/substrate/runtime/current-plugin.mjs.map +1 -1
  561. package/dist/substrate/runtime/errors.mjs +1 -13
  562. package/dist/substrate/runtime/errors.mjs.map +1 -1
  563. package/dist/substrate/runtime/format-unknown-error.mjs.map +1 -1
  564. package/dist/substrate/runtime/host-bind-mount-owner.mjs.map +1 -1
  565. package/dist/substrate/runtime/host-gateway.mjs.map +1 -1
  566. package/dist/substrate/runtime/host-tree-tar/index.mjs +15 -163
  567. package/dist/substrate/runtime/host-tree-tar/index.mjs.map +1 -1
  568. package/dist/substrate/runtime/http-probe.d.mts +1 -3
  569. package/dist/substrate/runtime/http-probe.mjs.map +1 -1
  570. package/dist/substrate/runtime/index.mjs +1 -6
  571. package/dist/substrate/runtime/lease-broker/service.mjs +5 -23
  572. package/dist/substrate/runtime/lease-broker/service.mjs.map +1 -1
  573. package/dist/substrate/runtime/lifecycle/dep-graph.mjs +3 -3
  574. package/dist/substrate/runtime/lifecycle/dep-graph.mjs.map +1 -1
  575. package/dist/substrate/runtime/lifecycle/graph-input-id.d.mts +1 -0
  576. package/dist/substrate/runtime/lifecycle/graph-input-id.mjs +133 -0
  577. package/dist/substrate/runtime/lifecycle/graph-input-id.mjs.map +1 -0
  578. package/dist/substrate/runtime/lifecycle/index.mjs +2 -2
  579. package/dist/substrate/runtime/lifecycle/plugin-registry.d.mts +1 -1
  580. package/dist/substrate/runtime/lifecycle/plugin-registry.mjs +22 -1
  581. package/dist/substrate/runtime/lifecycle/plugin-registry.mjs.map +1 -1
  582. package/dist/substrate/runtime/lifecycle/ready-gate.mjs +2 -15
  583. package/dist/substrate/runtime/lifecycle/ready-gate.mjs.map +1 -1
  584. package/dist/substrate/runtime/lifecycle/selective-restart.mjs +29 -25
  585. package/dist/substrate/runtime/lifecycle/selective-restart.mjs.map +1 -1
  586. package/dist/substrate/runtime/lifecycle/signals.mjs +1 -1
  587. package/dist/substrate/runtime/lifecycle/signals.mjs.map +1 -1
  588. package/dist/substrate/runtime/lifecycle/state-machine.mjs.map +1 -1
  589. package/dist/substrate/runtime/lifecycle/watch-attribution.d.mts +1 -1
  590. package/dist/substrate/runtime/lifecycle/watch-attribution.mjs +0 -1
  591. package/dist/substrate/runtime/lifecycle/watch-attribution.mjs.map +1 -1
  592. package/dist/substrate/runtime/managed-container.d.mts +5 -1
  593. package/dist/substrate/runtime/managed-container.mjs +8 -10
  594. package/dist/substrate/runtime/managed-container.mjs.map +1 -1
  595. package/dist/substrate/runtime/manifest/manifest.mjs +8 -30
  596. package/dist/substrate/runtime/manifest/manifest.mjs.map +1 -1
  597. package/dist/substrate/runtime/mode-errors.d.mts +1 -2
  598. package/dist/substrate/runtime/mode-errors.mjs.map +1 -1
  599. package/dist/substrate/runtime/observability/cascade-formatter.mjs +1 -11
  600. package/dist/substrate/runtime/observability/cascade-formatter.mjs.map +1 -1
  601. package/dist/substrate/runtime/observability/ignore-with-log.mjs.map +1 -1
  602. package/dist/substrate/runtime/observability/index.d.mts +1 -1
  603. package/dist/substrate/runtime/observability/index.mjs +4 -6
  604. package/dist/substrate/runtime/observability/log-attrs.mjs +46 -0
  605. package/dist/substrate/runtime/observability/log-attrs.mjs.map +1 -0
  606. package/dist/substrate/runtime/observability/log-store.mjs.map +1 -1
  607. package/dist/substrate/runtime/observability/logger.mjs +3 -3
  608. package/dist/substrate/runtime/observability/logger.mjs.map +1 -1
  609. package/dist/substrate/runtime/observability/output-truncate.mjs.map +1 -1
  610. package/dist/substrate/runtime/observability/pretty-error.mjs.map +1 -1
  611. package/dist/substrate/runtime/observability/process-lines.mjs.map +1 -1
  612. package/dist/substrate/runtime/observability/redaction.mjs.map +1 -1
  613. package/dist/substrate/runtime/observability/subprocess-capture.mjs +2 -2
  614. package/dist/substrate/runtime/observability/subprocess-capture.mjs.map +1 -1
  615. package/dist/substrate/runtime/passthrough-or-wrap.mjs.map +1 -1
  616. package/dist/substrate/runtime/paths.mjs +27 -9
  617. package/dist/substrate/runtime/paths.mjs.map +1 -1
  618. package/dist/substrate/runtime/phase-preserving-produce.mjs.map +1 -1
  619. package/dist/substrate/runtime/port-broker/service.mjs +10 -27
  620. package/dist/substrate/runtime/port-broker/service.mjs.map +1 -1
  621. package/dist/substrate/runtime/post-acquire-tasks.mjs.map +1 -1
  622. package/dist/substrate/runtime/probes.d.mts +2 -3
  623. package/dist/substrate/runtime/probes.mjs.map +1 -1
  624. package/dist/substrate/runtime/process-supervisor.mjs.map +1 -1
  625. package/dist/substrate/runtime/projection/operational-endpoints.mjs.map +1 -1
  626. package/dist/substrate/runtime/projection/state-ref.mjs.map +1 -1
  627. package/dist/substrate/runtime/projection/update.mjs +87 -11
  628. package/dist/substrate/runtime/projection/update.mjs.map +1 -1
  629. package/dist/substrate/runtime/random-suffix.mjs.map +1 -1
  630. package/dist/substrate/runtime/reconcile/fs-plan.mjs +146 -0
  631. package/dist/substrate/runtime/reconcile/fs-plan.mjs.map +1 -0
  632. package/dist/substrate/runtime/reconcile/graph.mjs +19 -0
  633. package/dist/substrate/runtime/reconcile/graph.mjs.map +1 -0
  634. package/dist/substrate/runtime/reconcile/index.mjs +5 -0
  635. package/dist/substrate/runtime/reconcile/label.mjs +41 -0
  636. package/dist/substrate/runtime/reconcile/label.mjs.map +1 -0
  637. package/dist/substrate/runtime/reconcile/spec.mjs +15 -0
  638. package/dist/substrate/runtime/reconcile/spec.mjs.map +1 -0
  639. package/dist/substrate/runtime/retry-policy.d.mts +2 -3
  640. package/dist/substrate/runtime/retry-policy.mjs.map +1 -1
  641. package/dist/substrate/runtime/routed-url.mjs.map +1 -1
  642. package/dist/substrate/runtime/runtime-decode.mjs.map +1 -1
  643. package/dist/substrate/runtime/scoped-http-server.mjs.map +1 -1
  644. package/dist/substrate/runtime/stage-and-swap/index.mjs +49 -12
  645. package/dist/substrate/runtime/stage-and-swap/index.mjs.map +1 -1
  646. package/dist/substrate/runtime/strategy-registry/chain-keyed-strategy-for.mjs.map +1 -1
  647. package/dist/substrate/runtime/strategy-registry/chain-probe-for.mjs.map +1 -1
  648. package/dist/substrate/runtime/strategy-registry/service.mjs +29 -17
  649. package/dist/substrate/runtime/strategy-registry/service.mjs.map +1 -1
  650. package/dist/substrate/runtime/subnet-broker.mjs.map +1 -1
  651. package/dist/substrate/runtime/supervisor/acquire-node.mjs +188 -56
  652. package/dist/substrate/runtime/supervisor/acquire-node.mjs.map +1 -1
  653. package/dist/substrate/runtime/supervisor/background-tasks.mjs +79 -65
  654. package/dist/substrate/runtime/supervisor/background-tasks.mjs.map +1 -1
  655. package/dist/substrate/runtime/supervisor/command-loop.mjs +82 -23
  656. package/dist/substrate/runtime/supervisor/command-loop.mjs.map +1 -1
  657. package/dist/substrate/runtime/supervisor/contribution-dispatcher.mjs +16 -0
  658. package/dist/substrate/runtime/supervisor/contribution-dispatcher.mjs.map +1 -0
  659. package/dist/substrate/runtime/supervisor/errors.mjs +11 -2
  660. package/dist/substrate/runtime/supervisor/errors.mjs.map +1 -1
  661. package/dist/substrate/runtime/supervisor/index.mjs +2 -0
  662. package/dist/substrate/runtime/supervisor/shutdown.mjs +6 -6
  663. package/dist/substrate/runtime/supervisor/shutdown.mjs.map +1 -1
  664. package/dist/substrate/runtime/supervisor/start-supervisor.mjs +60 -70
  665. package/dist/substrate/runtime/supervisor/start-supervisor.mjs.map +1 -1
  666. package/dist/substrate/runtime/supervisor/state.d.mts +1 -1
  667. package/dist/substrate/runtime/supervisor/state.mjs +1 -1
  668. package/dist/substrate/runtime/supervisor/state.mjs.map +1 -1
  669. package/dist/substrate/runtime/supervisor/teardown.mjs +10 -9
  670. package/dist/substrate/runtime/supervisor/teardown.mjs.map +1 -1
  671. package/dist/substrate/runtime/supervisor/wiring.mjs +6 -20
  672. package/dist/substrate/runtime/supervisor/wiring.mjs.map +1 -1
  673. package/dist/substrate/runtime/tar/reader.mjs +216 -0
  674. package/dist/substrate/runtime/tar/reader.mjs.map +1 -0
  675. package/dist/substrate/runtime/typed-env.mjs.map +1 -1
  676. package/dist/substrate/versioned-doc-schema.mjs.map +1 -1
  677. package/dist/substrate/versioned-doc-sync.mjs +5 -5
  678. package/dist/substrate/versioned-doc-sync.mjs.map +1 -1
  679. package/dist/surfaces/cli/command-tree.mjs +49 -7
  680. package/dist/surfaces/cli/command-tree.mjs.map +1 -1
  681. package/dist/surfaces/cli/commands/config.mjs +1 -1
  682. package/dist/surfaces/cli/commands/config.mjs.map +1 -1
  683. package/dist/surfaces/cli/commands/confirm-node.mjs.map +1 -1
  684. package/dist/surfaces/cli/commands/confirm.mjs.map +1 -1
  685. package/dist/surfaces/cli/commands/doctor.mjs +1 -1
  686. package/dist/surfaces/cli/commands/doctor.mjs.map +1 -1
  687. package/dist/surfaces/cli/commands/prune-picker-entry.mjs.map +1 -1
  688. package/dist/surfaces/cli/commands/prune-picker.mjs.map +1 -1
  689. package/dist/surfaces/cli/commands/prune.mjs +1 -1
  690. package/dist/surfaces/cli/commands/prune.mjs.map +1 -1
  691. package/dist/surfaces/cli/commands/snapshot.mjs +1 -1
  692. package/dist/surfaces/cli/commands/snapshot.mjs.map +1 -1
  693. package/dist/surfaces/cli/commands/status.mjs +1 -1
  694. package/dist/surfaces/cli/commands/status.mjs.map +1 -1
  695. package/dist/surfaces/cli/commands/supervisor-presence.mjs.map +1 -1
  696. package/dist/surfaces/cli/commands/wipe.mjs +3 -4
  697. package/dist/surfaces/cli/commands/wipe.mjs.map +1 -1
  698. package/dist/surfaces/cli/envelope.mjs.map +1 -1
  699. package/dist/surfaces/cli/errors.mjs.map +1 -1
  700. package/dist/surfaces/cli/flags.mjs.map +1 -1
  701. package/dist/surfaces/cli/index.mjs +45 -14
  702. package/dist/surfaces/cli/index.mjs.map +1 -1
  703. package/dist/surfaces/cli/output.mjs.map +1 -1
  704. package/dist/surfaces/cli/sysexits.mjs.map +1 -1
  705. package/dist/surfaces/tui/app.mjs +0 -21
  706. package/dist/surfaces/tui/app.mjs.map +1 -1
  707. package/dist/surfaces/tui/dashboard.mjs +0 -48
  708. package/dist/surfaces/tui/dashboard.mjs.map +1 -1
  709. package/dist/surfaces/tui/display-derivation.mjs +80 -16
  710. package/dist/surfaces/tui/display-derivation.mjs.map +1 -1
  711. package/dist/surfaces/tui/errors.mjs.map +1 -1
  712. package/dist/surfaces/tui/event-log.mjs +0 -9
  713. package/dist/surfaces/tui/event-log.mjs.map +1 -1
  714. package/dist/surfaces/tui/heartbeat.mjs.map +1 -1
  715. package/dist/surfaces/tui/index.mjs +2 -2
  716. package/dist/surfaces/tui/index.mjs.map +1 -1
  717. package/dist/surfaces/tui/input.mjs.map +1 -1
  718. package/dist/surfaces/tui/mode-detect.mjs.map +1 -1
  719. package/dist/surfaces/tui/mount-ink.mjs.map +1 -1
  720. package/dist/surfaces/tui/plain-renderer.mjs +26 -19
  721. package/dist/surfaces/tui/plain-renderer.mjs.map +1 -1
  722. package/dist/surfaces/tui/resource-table.mjs +8 -10
  723. package/dist/surfaces/tui/resource-table.mjs.map +1 -1
  724. package/images/postgres/Dockerfile +0 -1
  725. package/images/sui/Dockerfile +27 -54
  726. package/images/sui/entrypoint.sh +17 -179
  727. package/package.json +23 -15
  728. package/dashboard-ui/assets/index-Bmi1UtAg.js +0 -1356
  729. package/dashboard-ui/assets/index-D5EShVt4.js +0 -3
  730. package/dashboard-ui/assets/index-Deml9drg.css +0 -1
  731. package/dist/api/plugin-errors.d.mts +0 -7
  732. package/dist/api/plugin-errors.mjs +0 -10
  733. package/dist/api/plugin-errors.mjs.map +0 -1
  734. package/dist/cli/wirings/build-verb-layers.mjs +0 -42
  735. package/dist/cli/wirings/build-verb-layers.mjs.map +0 -1
  736. package/dist/contracts/capability-decl.d.mts +0 -40
  737. package/dist/orchestrators/built-in-plugin-layers.mjs +0 -54
  738. package/dist/orchestrators/built-in-plugin-layers.mjs.map +0 -1
  739. package/dist/orchestrators/run.mjs +0 -91
  740. package/dist/orchestrators/run.mjs.map +0 -1
  741. package/dist/orchestrators/runtime-composition.d.mts +0 -10
  742. package/dist/orchestrators/runtime-composition.mjs +0 -252
  743. package/dist/orchestrators/runtime-composition.mjs.map +0 -1
  744. package/dist/orchestrators/snapshot/capture-command.d.mts +0 -1
  745. package/dist/orchestrators/snapshot/capture-command.mjs +0 -25
  746. package/dist/orchestrators/snapshot/capture-command.mjs.map +0 -1
  747. package/dist/orchestrators/snapshot/pending-marker.mjs +0 -99
  748. package/dist/orchestrators/snapshot/pending-marker.mjs.map +0 -1
  749. package/dist/orchestrators/snapshot/recover-pending.mjs +0 -223
  750. package/dist/orchestrators/snapshot/recover-pending.mjs.map +0 -1
  751. package/dist/orchestrators/snapshot/state-document.d.mts +0 -1
  752. package/dist/orchestrators/snapshot/state-document.mjs +0 -58
  753. package/dist/orchestrators/snapshot/state-document.mjs.map +0 -1
  754. package/dist/plugins/account/spans.mjs +0 -16
  755. package/dist/plugins/account/spans.mjs.map +0 -1
  756. package/dist/plugins/account/variants/env.mjs +0 -24
  757. package/dist/plugins/account/variants/env.mjs.map +0 -1
  758. package/dist/plugins/account/variants/inline.mjs +0 -13
  759. package/dist/plugins/account/variants/inline.mjs.map +0 -1
  760. package/dist/plugins/account/variants/keystore.mjs +0 -91
  761. package/dist/plugins/account/variants/keystore.mjs.map +0 -1
  762. package/dist/plugins/action/spans.mjs +0 -11
  763. package/dist/plugins/action/spans.mjs.map +0 -1
  764. package/dist/plugins/coin/spans.mjs +0 -20
  765. package/dist/plugins/coin/spans.mjs.map +0 -1
  766. package/dist/plugins/deepbook/spans.mjs +0 -18
  767. package/dist/plugins/deepbook/spans.mjs.map +0 -1
  768. package/dist/plugins/faucet/spans.mjs +0 -12
  769. package/dist/plugins/faucet/spans.mjs.map +0 -1
  770. package/dist/plugins/package/spans.mjs +0 -14
  771. package/dist/plugins/package/spans.mjs.map +0 -1
  772. package/dist/plugins/postgres/codegen.mjs +0 -42
  773. package/dist/plugins/postgres/codegen.mjs.map +0 -1
  774. package/dist/plugins/postgres/connection.d.mts +0 -33
  775. package/dist/plugins/postgres/connection.mjs.map +0 -1
  776. package/dist/plugins/postgres/db-ensure.mjs.map +0 -1
  777. package/dist/plugins/postgres/errors.d.mts +0 -60
  778. package/dist/plugins/postgres/errors.mjs +0 -29
  779. package/dist/plugins/postgres/errors.mjs.map +0 -1
  780. package/dist/plugins/postgres/index.d.mts +0 -37
  781. package/dist/plugins/postgres/index.mjs +0 -68
  782. package/dist/plugins/postgres/index.mjs.map +0 -1
  783. package/dist/plugins/postgres/routable.mjs +0 -33
  784. package/dist/plugins/postgres/routable.mjs.map +0 -1
  785. package/dist/plugins/postgres/service.d.mts +0 -50
  786. package/dist/plugins/postgres/service.mjs +0 -179
  787. package/dist/plugins/postgres/service.mjs.map +0 -1
  788. package/dist/plugins/postgres/snapshot.mjs +0 -32
  789. package/dist/plugins/postgres/snapshot.mjs.map +0 -1
  790. package/dist/plugins/postgres/spans.mjs +0 -11
  791. package/dist/plugins/postgres/spans.mjs.map +0 -1
  792. package/dist/plugins/seal/spans.mjs +0 -18
  793. package/dist/plugins/seal/spans.mjs.map +0 -1
  794. package/dist/plugins/sui/spans.mjs +0 -17
  795. package/dist/plugins/sui/spans.mjs.map +0 -1
  796. package/dist/plugins/wallet/spans.mjs +0 -22
  797. package/dist/plugins/wallet/spans.mjs.map +0 -1
  798. package/dist/plugins/walrus/registry-publish.d.mts +0 -24
  799. package/dist/plugins/walrus/spans.mjs +0 -18
  800. package/dist/plugins/walrus/spans.mjs.map +0 -1
  801. package/dist/runtime/docker/logs.d.mts +0 -1
  802. package/dist/runtime/docker/logs.mjs +0 -34
  803. package/dist/runtime/docker/logs.mjs.map +0 -1
  804. package/dist/substrate/runtime/artifact-publisher/index.mjs +0 -86
  805. package/dist/substrate/runtime/artifact-publisher/index.mjs.map +0 -1
  806. package/dist/substrate/runtime/capability-sinks/index.d.mts +0 -1
  807. package/dist/substrate/runtime/capability-sinks/index.mjs +0 -3
  808. package/dist/substrate/runtime/capability-sinks/layer.d.mts +0 -1
  809. package/dist/substrate/runtime/capability-sinks/layer.mjs +0 -31
  810. package/dist/substrate/runtime/capability-sinks/layer.mjs.map +0 -1
  811. package/dist/substrate/runtime/capability-sinks/service.d.mts +0 -95
  812. package/dist/substrate/runtime/capability-sinks/service.mjs +0 -69
  813. package/dist/substrate/runtime/capability-sinks/service.mjs.map +0 -1
  814. package/dist/substrate/runtime/cross-process/lock.d.mts +0 -1
  815. package/dist/substrate/runtime/cross-process/lock.mjs +0 -23
  816. package/dist/substrate/runtime/cross-process/lock.mjs.map +0 -1
  817. package/dist/substrate/runtime/cross-process/snapshot-reservation.mjs +0 -113
  818. package/dist/substrate/runtime/cross-process/snapshot-reservation.mjs.map +0 -1
  819. package/dist/substrate/runtime/lifecycle/lifecycle-fact.mjs +0 -31
  820. package/dist/substrate/runtime/lifecycle/lifecycle-fact.mjs.map +0 -1
  821. package/dist/substrate/runtime/observability/formatter-registry.d.mts +0 -1
  822. package/dist/substrate/runtime/observability/formatter-registry.mjs +0 -48
  823. package/dist/substrate/runtime/observability/formatter-registry.mjs.map +0 -1
  824. package/dist/substrate/runtime/observability/span-store.d.mts +0 -1
  825. package/dist/substrate/runtime/observability/span-store.mjs +0 -110
  826. package/dist/substrate/runtime/observability/span-store.mjs.map +0 -1
  827. package/dist/substrate/runtime/observability/spans.d.mts +0 -1
  828. package/dist/substrate/runtime/observability/spans.mjs +0 -87
  829. package/dist/substrate/runtime/observability/spans.mjs.map +0 -1
  830. package/dist/substrate/runtime/projection/index.mjs +0 -4
  831. package/dist/substrate/runtime/projection/persisted.mjs +0 -213
  832. package/dist/substrate/runtime/projection/persisted.mjs.map +0 -1
  833. package/dist/substrate/runtime/scoped-multimap/service.mjs +0 -52
  834. package/dist/substrate/runtime/scoped-multimap/service.mjs.map +0 -1
  835. package/dist/substrate/runtime/scoped-ref-map/index.mjs +0 -2
  836. package/dist/substrate/runtime/scoped-ref-map/service.mjs +0 -83
  837. package/dist/substrate/runtime/scoped-ref-map/service.mjs.map +0 -1
  838. package/dist/substrate/runtime/state-store/index.mjs +0 -3
  839. package/dist/substrate/runtime/state-store/schema.d.mts +0 -1
  840. package/dist/substrate/runtime/state-store/schema.mjs +0 -41
  841. package/dist/substrate/runtime/state-store/schema.mjs.map +0 -1
  842. package/dist/substrate/runtime/state-store/service.d.mts +0 -1
  843. package/dist/substrate/runtime/state-store/service.mjs +0 -145
  844. package/dist/substrate/runtime/state-store/service.mjs.map +0 -1
  845. package/dist/substrate/runtime/sui-execute/index.mjs.map +0 -1
  846. package/dist/substrate/runtime/sui-execute/sign-and-dispatch.mjs.map +0 -1
  847. package/dist/substrate/runtime/sui-ledger/object-ref.mjs.map +0 -1
  848. package/dist/substrate/runtime/sui-move-build/index.mjs.map +0 -1
  849. package/dist/substrate/runtime/supervisor/dispatch-contributions.mjs +0 -151
  850. package/dist/substrate/runtime/supervisor/dispatch-contributions.mjs.map +0 -1
  851. package/dist/substrate/state-store.d.mts +0 -1
  852. /package/dist/orchestrators/{snapshot/pending-marker.d.mts → codegen/id-config.d.mts} +0 -0
  853. /package/dist/orchestrators/snapshot/{recover-pending.d.mts → interrupted-restore.d.mts} +0 -0
  854. /package/dist/{substrate/runtime/sui-execute → plugins/sui/exec}/sign-and-dispatch.d.mts +0 -0
  855. /package/dist/plugins/{postgres/snapshot.d.mts → sui/ledger/object-ref.d.mts} +0 -0
  856. /package/dist/{substrate/runtime/sui-move-build → plugins/sui/move}/index.d.mts +0 -0
  857. /package/dist/substrate/runtime/{state-store/index.d.mts → reconcile/graph.d.mts} +0 -0
  858. /package/dist/substrate/runtime/{cross-process/snapshot-reservation.d.mts → supervisor/contribution-dispatcher.d.mts} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"service.mjs","names":[],"sources":["../../../../src/substrate/runtime/control-plane/service.ts"],"sourcesContent":["// Control-plane seam exposed to in-process surfaces (the dashboard plugin).\n//\n// Plugins don't normally receive the supervisor's projection ref, event\n// hub, or command channel — those live in `SupervisorState`. This service\n// hands an in-process surface (the dashboard) a read view of the live\n// projection plus a fire-and-forget command publisher, so it can render\n// state and issue controls without reaching into supervisor internals.\n//\n// The projection ref is process-scoped and survives engine cycles, so a\n// subscriber stays valid across `stack.restart` (only `cycle.id` bumps).\n// Command dispatch mirrors the queue publisher the TUI uses: offer onto\n// the supervisor's command queue and observe the effect via `state`.\n//\n// The `domain` surface is GENERIC and name-blind: it exposes the snapshot\n// catalog + restore/delete (which never round-trip through the void\n// `publishCommand`), the cross-service observability rings, and a single\n// generic `resolvedValues` accessor that hands out resolved plugin VALUES\n// WITHOUT interpreting them. Plugin-name-aware shaping (deepbook/seal/coin/\n// postgres/mode/mint) lives ABOVE the substrate in the dashboard plugin,\n// which is allowed to name plugins. The projection (`SubscribableState`)\n// stays CLOSED — none of this leaks into it. Each accessor is a\n// self-contained `Effect` the supervisor populates from the resolved\n// registry / snapshot orchestrator it holds at wiring time.\n\nimport { Context, type Effect, type SubscriptionRef } from 'effect';\nimport type { EngineCommand } from '../../events.ts';\nimport type { SubscribableState } from '../../projection.ts';\nimport type {\n\tLogFilter,\n\tLogRecord,\n\tSpanFilter,\n\tSpanRecord,\n} from '../observability/index.ts';\n\n// -----------------------------------------------------------------------------\n// Domain data shapes — app-agnostic projections of plugin-resolved values.\n// These intentionally stay generic so the control-plane surface and the\n// dashboard schema stay decoupled from plugin internals.\n// -----------------------------------------------------------------------------\n\n/** One catalog entry the snapshot orchestrator's `list` produced. */\nexport interface ControlPlaneSnapshotEntry {\n\treadonly id: string;\n\treadonly label: string | null;\n\treadonly createdAt: number | null;\n\treadonly app: string | null;\n\treadonly stack: string | null;\n\treadonly network: string | null;\n\treadonly participants: ReadonlyArray<string>;\n\treadonly containerCount: number;\n\treadonly subtreeCount: number;\n\t/** `true` when the metadata file was absent/corrupt (do-not-trust). */\n\treadonly corrupt: boolean;\n}\n\n/** One resolved plugin value, handed out GENERICALLY (uninterpreted) so an\n * in-process surface above the substrate (the dashboard plugin) can match\n * + shape it by resource-id prefix itself. `id` is the resource id the\n * plugin factory minted (an opaque string here — the substrate never\n * pattern-matches it); `pluginKey` is the registry key for the node. The\n * substrate imports NO plugin types — `value` is genuinely `unknown`. */\nexport interface ControlPlaneResolvedValue {\n\t/** The registry key for the resolved node. */\n\treadonly pluginKey: string;\n\t/** The resource id the plugin factory minted (opaque to the substrate). */\n\treadonly id: string;\n\t/** The resolved plugin value, uninterpreted. */\n\treadonly value: unknown;\n}\n\n/** The plugin-domain accessor surface. GENERIC + name-blind: it exposes\n * the snapshot catalog, the observability rings, and a single generic\n * `resolvedValues` accessor. Each member is a self-contained Effect the\n * supervisor wires from the data it holds at boot. The Effects never fail\n * (`E = never`); they degrade to empty/`null` so a single missing plugin\n * can't take down the dashboard query. */\nexport interface ControlPlaneDomain {\n\t/** Snapshot catalog (id/label/created/participants/containers). */\n\treadonly snapshots: Effect.Effect<ReadonlyArray<ControlPlaneSnapshotEntry>>;\n\t/** Restore a snapshot by id. Mirrors the orchestrator surface so the\n\t * dashboard gets a real result the void `publishCommand` can't carry. */\n\treadonly restoreSnapshot: (id: string) => Effect.Effect<{ readonly ok: boolean; readonly detail: string | null }>;\n\t/** Delete a snapshot by id. */\n\treadonly deleteSnapshot: (id: string) => Effect.Effect<{ readonly ok: boolean; readonly detail: string | null }>;\n\t/** ALL resolved plugin values, in graph order, handed out\n\t * uninterpreted. The seam an in-process surface above the substrate\n\t * (the dashboard plugin) uses to find + shape plugin-domain values\n\t * itself — the substrate stays name-blind. */\n\treadonly resolvedValues: Effect.Effect<ReadonlyArray<ControlPlaneResolvedValue>>;\n\t/** Cross-service queryable log history (the dashboard Console \"Logs\"\n\t * tab). Filterable by service / level / substring / time window; the\n\t * per-row projection tail is a separate, row-scoped surface. Degrades\n\t * to empty when no log store is wired. */\n\treadonly logs: (filter?: LogFilter) => Effect.Effect<ReadonlyArray<LogRecord>>;\n\t/** Distinct services currently present in the log ring (filter UI). */\n\treadonly logServices: Effect.Effect<ReadonlyArray<string>>;\n\t/** Completed-span ring (the dashboard Console \"Traces\" tab). Filterable\n\t * by service / status / substring / time window. Degrades to empty when\n\t * no span store is wired. */\n\treadonly spans: (filter?: SpanFilter) => Effect.Effect<ReadonlyArray<SpanRecord>>;\n\t/** Distinct services currently present in the span ring (filter UI). */\n\treadonly spanServices: Effect.Effect<ReadonlyArray<string>>;\n}\n\nexport interface ControlPlane {\n\t/** Live projection ref. Read a snapshot with `SubscriptionRef.get`,\n\t * subscribe to updates with `SubscriptionRef.changes`. */\n\treadonly state: SubscriptionRef.SubscriptionRef<SubscribableState>;\n\t/** Publish a command to the supervisor (fire-and-forget). Observe the\n\t * resulting effect via the projection `state`. */\n\treadonly publishCommand: (command: EngineCommand) => Effect.Effect<void>;\n\t/** Generic, name-blind plugin-domain accessors (snapshot catalog,\n\t * observability rings, resolved plugin values). See\n\t * `ControlPlaneDomain`. */\n\treadonly domain: ControlPlaneDomain;\n}\n\nexport class ControlPlaneService extends Context.Service<ControlPlaneService, ControlPlane>()(\n\t'@devstack/substrate/ControlPlane',\n) {}\n"],"mappings":";;AAqHA,IAAa,sBAAb,cAAyC,QAAQ,SAA4C,CAC5F,mCACA,CAAC"}
1
+ {"version":3,"file":"service.mjs","names":[],"sources":["../../../../src/substrate/runtime/control-plane/service.ts"],"sourcesContent":["// Control-plane seam exposed to in-process surfaces (the dashboard plugin).\n//\n// Plugins don't normally receive the supervisor's projection ref, event\n// hub, or command channel — those live in `SupervisorState`. This service\n// hands an in-process surface (the dashboard) a read view of the live\n// projection plus a fire-and-forget command publisher, so it can render\n// state and issue controls without reaching into supervisor internals.\n//\n// The projection ref is process-scoped and survives engine cycles, so a\n// subscriber stays valid across `stack.restart` (only `cycle.id` bumps).\n// Command dispatch mirrors the queue publisher the TUI uses: offer onto\n// the supervisor's command queue and observe the effect via `state`.\n//\n// The `domain` surface is GENERIC and name-blind: it exposes the snapshot\n// catalog + restore/delete (which never round-trip through the void\n// `publishCommand`), the cross-service observability rings, and a single\n// generic `resolvedValues` accessor that hands out resolved plugin VALUES\n// WITHOUT interpreting them. Plugin-name-aware shaping (deepbook/seal/coin/\n// mode/mint/plugin-specific shaping lives ABOVE the substrate in the dashboard plugin,\n// which is allowed to name plugins. The projection (`SubscribableState`)\n// stays CLOSED — none of this leaks into it. Each accessor is a\n// self-contained `Effect` the supervisor populates from the resolved\n// registry / snapshot orchestrator it holds at wiring time.\n\nimport { Context, type Effect, type SubscriptionRef } from 'effect';\nimport type { EngineCommand } from '../../events.ts';\nimport type { SubscribableState } from '../../projection.ts';\nimport type { LogFilter, LogRecord } from '../observability/index.ts';\n\n// -----------------------------------------------------------------------------\n// Domain data shapes — app-agnostic projections of plugin-resolved values.\n// These intentionally stay generic so the control-plane surface and the\n// dashboard schema stay decoupled from plugin internals.\n// -----------------------------------------------------------------------------\n\n/** One catalog entry the snapshot orchestrator's `list` produced. */\nexport interface ControlPlaneSnapshotEntry {\n\treadonly id: string;\n\treadonly label: string | null;\n\treadonly createdAt: number | null;\n\treadonly app: string | null;\n\treadonly stack: string | null;\n\treadonly network: string | null;\n\treadonly snapshotGraphInputId: string | null;\n\treadonly currentGraphInputId: string | null;\n\treadonly graphInputStatus: 'matching' | 'stale' | 'unknown';\n\treadonly graphInputWarning: string | null;\n\treadonly participants: ReadonlyArray<string>;\n\treadonly containerCount: number;\n\treadonly subtreeCount: number;\n\t/** `true` when the metadata file was absent/corrupt (do-not-trust). */\n\treadonly corrupt: boolean;\n}\n\n/** One resolved plugin value, handed out GENERICALLY (uninterpreted) so an\n * in-process surface above the substrate (the dashboard plugin) can match\n * + shape it by resource-id prefix itself. `id` is the resource id the\n * plugin factory minted (an opaque string here — the substrate never\n * pattern-matches it); `pluginKey` is the registry key for the node. The\n * substrate imports NO plugin types — `value` is genuinely `unknown`. */\nexport interface ControlPlaneResolvedValue {\n\t/** The registry key for the resolved node. */\n\treadonly pluginKey: string;\n\t/** The resource id the plugin factory minted (opaque to the substrate). */\n\treadonly id: string;\n\t/** The resolved plugin value, uninterpreted. */\n\treadonly value: unknown;\n}\n\n/** The plugin-domain accessor surface. GENERIC + name-blind: it exposes\n * the snapshot catalog, the observability rings, and a single generic\n * `resolvedValues` accessor. Each member is a self-contained Effect the\n * supervisor wires from the data it holds at boot. The Effects never fail\n * (`E = never`); they degrade to empty/`null` so a single missing plugin\n * can't take down the dashboard query. */\nexport interface ControlPlaneDomain {\n\t/** Snapshot catalog (id/label/created/participants/containers). */\n\treadonly snapshots: Effect.Effect<ReadonlyArray<ControlPlaneSnapshotEntry>>;\n\t/** Restore a snapshot by id. Mirrors the orchestrator surface so the\n\t * dashboard gets a real result the void `publishCommand` can't carry. */\n\treadonly restoreSnapshot: (\n\t\tid: string,\n\t) => Effect.Effect<{ readonly ok: boolean; readonly detail: string | null }>;\n\t/** Delete a snapshot by id. */\n\treadonly deleteSnapshot: (\n\t\tid: string,\n\t) => Effect.Effect<{ readonly ok: boolean; readonly detail: string | null }>;\n\t/** ALL resolved plugin values, in graph order, handed out\n\t * uninterpreted. The seam an in-process surface above the substrate\n\t * (the dashboard plugin) uses to find + shape plugin-domain values\n\t * itself — the substrate stays name-blind. */\n\treadonly resolvedValues: Effect.Effect<ReadonlyArray<ControlPlaneResolvedValue>>;\n\t/** Cross-service queryable log history (the dashboard Console \"Logs\"\n\t * tab). Filterable by service / level / substring / time window; the\n\t * per-row projection tail is a separate, row-scoped surface. Degrades\n\t * to empty when no log store is wired. */\n\treadonly logs: (filter?: LogFilter) => Effect.Effect<ReadonlyArray<LogRecord>>;\n\t/** Distinct services currently present in the log ring (filter UI). */\n\treadonly logServices: Effect.Effect<ReadonlyArray<string>>;\n}\n\nexport interface ControlPlane {\n\t/** Live projection ref. Read a snapshot with `SubscriptionRef.get`,\n\t * subscribe to updates with `SubscriptionRef.changes`. */\n\treadonly state: SubscriptionRef.SubscriptionRef<SubscribableState>;\n\t/** Publish a command to the supervisor (fire-and-forget). Observe the\n\t * resulting effect via the projection `state`. */\n\treadonly publishCommand: (command: EngineCommand) => Effect.Effect<void>;\n\t/** Submit a command to the supervisor's command-loop and AWAIT its\n\t * completion. Unlike `publishCommand` (fire-and-forget), this offers a\n\t * *submitted* command carrying a completion deferred, so the caller\n\t * blocks on the single command-loop consumer's real exit — and the\n\t * destructive command (e.g. `snapshot.restore`, which removes live\n\t * managed containers then re-acquires) runs in-band with the loop, not\n\t * out-of-band against the live supervisor. Fails with the command-loop's\n\t * cause when the handler failed. The dashboard restore path uses this so\n\t * the mutation only resolves once restore + re-acquire actually\n\t * completed. */\n\treadonly submitCommand: (command: EngineCommand) => Effect.Effect<void, unknown>;\n\t/** Generic, name-blind plugin-domain accessors (snapshot catalog,\n\t * observability rings, resolved plugin values). See\n\t * `ControlPlaneDomain`. */\n\treadonly domain: ControlPlaneDomain;\n}\n\nexport class ControlPlaneService extends Context.Service<ControlPlaneService, ControlPlane>()(\n\t'@devstack/substrate/ControlPlane',\n) {}\n"],"mappings":";;AA6HA,IAAa,sBAAb,cAAyC,QAAQ,QAA2C,CAAC,CAC5F,kCACD,CAAC,CAAC,CAAC"}
@@ -1,6 +1,6 @@
1
- import { selfPid } from "../self-pid.mjs";
2
- import { decodeUnknownSync } from "../../runtime-decode.mjs";
3
1
  import { CommandRecordSchema, EventRecordSchema } from "./protocol.mjs";
2
+ import { decodeUnknownSync } from "../../runtime-decode.mjs";
3
+ import { selfPid } from "../self-pid.mjs";
4
4
  import { CommandChannelIoError, appendRecord, ensureFile, tailRecords } from "./file-channel.mjs";
5
5
  import { Effect, Option, Ref, Stream } from "effect";
6
6
  import { existsSync, statSync } from "node:fs";
@@ -119,34 +119,31 @@ const makeCommandChannelSubscriber = (paths, options = {}) => Effect.gen(functio
119
119
  event
120
120
  });
121
121
  });
122
- const ack = (correlatesTo, detail, payload) => Effect.gen(function* () {
123
- yield* writeEvent({
122
+ const publishReply = (correlatesTo, reply) => Effect.gen(function* () {
123
+ const seq = yield* nextSubSeq(state);
124
+ yield* writeEvent(reply.kind === "ack" ? {
124
125
  protocol: 1,
125
- seq: yield* nextSubSeq(state),
126
+ seq,
126
127
  at: Date.now(),
127
128
  kind: "ack",
128
129
  correlatesTo,
129
- ...detail !== void 0 ? { detail } : {},
130
- ...payload !== void 0 ? { payload } : {}
131
- });
132
- });
133
- const fail = (correlatesTo, message, detail, payload) => Effect.gen(function* () {
134
- yield* writeEvent({
130
+ ...reply.detail !== void 0 ? { detail: reply.detail } : {},
131
+ ...reply.payload !== void 0 ? { payload: reply.payload } : {}
132
+ } : {
135
133
  protocol: 1,
136
- seq: yield* nextSubSeq(state),
134
+ seq,
137
135
  at: Date.now(),
138
136
  kind: "error",
139
137
  correlatesTo,
140
- message,
141
- ...detail !== void 0 ? { detail } : {},
142
- ...payload !== void 0 ? { payload } : {}
138
+ message: reply.message,
139
+ ...reply.detail !== void 0 ? { detail: reply.detail } : {},
140
+ ...reply.payload !== void 0 ? { payload: reply.payload } : {}
143
141
  });
144
142
  });
145
143
  return {
146
144
  commands,
147
145
  publishEvent,
148
- ack,
149
- fail
146
+ publishReply
150
147
  };
151
148
  });
152
149
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"channel.mjs","names":["nodeHostname"],"sources":["../../../../../src/substrate/runtime/cross-process/command-channel/channel.ts"],"sourcesContent":["// High-level cross-process command channel.\n//\n// Two halves built on the same `commands.ndjson` + `events.ndjson` pair:\n//\n// - Publisher side (CLI / TUI / programmable API caller): appends to\n// `commands.ndjson`; tails `events.ndjson` so it can correlate\n// ack/error replies AND surface incoming engine events (for `logs`\n// and `status --watch`-style verbs).\n//\n// - Supervisor side: tails `commands.ndjson` to receive incoming\n// intents; appends to `events.ndjson` to broadcast engine events\n// and ack/error replies.\n//\n// Both halves are scope-bound — the underlying poll fiber is forked\n// into the surrounding Scope and stops when the Scope closes.\n\nimport { existsSync, statSync } from 'node:fs';\nimport { hostname as nodeHostname } from 'node:os';\nimport { randomUUID } from 'node:crypto';\n\nimport { Effect, Option, Ref, Stream, Scope } from 'effect';\n\nimport type { EngineCommand } from '../../../events.ts';\nimport { decodeUnknownSync } from '../../runtime-decode.ts';\nimport { selfPid } from '../self-pid.ts';\nimport {\n\ttype CommandChannelError,\n\tCommandChannelIoError,\n\tappendRecord,\n\tensureFile,\n\ttailRecords,\n} from './file-channel.ts';\nimport {\n\tCOMMAND_CHANNEL_PROTOCOL_VERSION,\n\tCommandRecordSchema,\n\tEventRecordSchema,\n\ttype CommandRecord,\n\ttype EventRecord,\n} from './protocol.ts';\n\nexport interface CommandChannelPaths {\n\treadonly commandsFile: string;\n\treadonly eventsFile: string;\n}\n\nexport const COMMAND_CHANNEL_COMMANDS_FILE_NAME = 'commands.ndjson';\nexport const COMMAND_CHANNEL_EVENTS_FILE_NAME = 'events.ndjson';\n\n/** Build a `command-channel` paths bundle from a stackRoot dir. The\n * files live next to the existing cross-process artifacts. */\nexport const commandChannelPaths = (stackRoot: string): CommandChannelPaths => ({\n\tcommandsFile: `${stackRoot}/${COMMAND_CHANNEL_COMMANDS_FILE_NAME}`,\n\teventsFile: `${stackRoot}/${COMMAND_CHANNEL_EVENTS_FILE_NAME}`,\n});\n\n// -----------------------------------------------------------------------------\n// Publisher side\n// -----------------------------------------------------------------------------\n\nexport interface PublishedCommand {\n\t/** Per-record id used to correlate `ack` / `error` replies. */\n\treadonly id: string;\n\t/** Byte offset of the events file at publish time. `awaitCompletion`\n\t * tails from here so each await scans only events appended after the\n\t * command was published, instead of replaying the whole file on every\n\t * call (O(N·total) rescans). A reply can only land at-or-after this\n\t * offset because the supervisor reads the command before acking. */\n\treadonly fromOffset: number;\n}\n\n/** Default bound for `awaitCompletion` when the caller passes no\n * `timeoutMillis`. A correlated ack/error normally lands within tens of\n * ms; an absent reply (supervisor died mid-handle, dropped record) must\n * NOT park the await fiber forever, so the wait is always bounded. */\nexport const DEFAULT_AWAIT_COMPLETION_MILLIS = 30_000;\n\n/**\n * Publisher half: append a typed command, optionally await its\n * corresponding ack/error reply.\n *\n * `publish(cmd)` writes one record and returns the id; the caller can\n * then `awaitCompletion(id)` to block until the supervisor either acks\n * or errors. `awaitCompletion` is a separate seam so verbs that don't\n * need correlation (TUI-style fire-and-forget) skip the wait.\n */\nexport interface CommandChannelPublisher {\n\treadonly publish: (\n\t\tcommand: EngineCommand,\n\t) => Effect.Effect<PublishedCommand, CommandChannelError>;\n\treadonly awaitCompletion: (\n\t\tpublished: PublishedCommand,\n\t\toptions?: { readonly timeoutMillis?: number },\n\t) => Effect.Effect<\n\t\t| { readonly ok: true; readonly payload?: unknown }\n\t\t| { readonly ok: false; readonly message: string; readonly payload?: unknown },\n\t\tCommandChannelError\n\t>;\n\treadonly events: Stream.Stream<EventRecord, CommandChannelError, Scope.Scope>;\n}\n\ninterface PublisherState {\n\treadonly seq: Ref.Ref<number>;\n}\n\nconst nextSeq = (state: PublisherState): Effect.Effect<number> =>\n\tRef.modify(state.seq, (n) => [n + 1, n + 1]);\n\n/**\n * Build the publisher half. The events Stream is scope-bound:\n * pass it through `Stream.runDrain` inside a `Scope.use` or fork it\n * with `Effect.forkScoped` so the tail polling cleans up.\n */\nexport const makeCommandChannelPublisher = (\n\tpaths: CommandChannelPaths,\n): Effect.Effect<CommandChannelPublisher, CommandChannelError> =>\n\tEffect.gen(function* () {\n\t\tyield* ensureFile(paths.commandsFile);\n\t\tyield* ensureFile(paths.eventsFile);\n\t\tconst state: PublisherState = { seq: yield* Ref.make(0) };\n\t\tconst pid = selfPid();\n\t\tconst host = nodeHostname();\n\n\t\tconst publish = (\n\t\t\tcommand: EngineCommand,\n\t\t): Effect.Effect<PublishedCommand, CommandChannelError> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst seq = yield* nextSeq(state);\n\t\t\t\tconst id = `${pid}-${seq}-${randomUUID().slice(0, 8)}`;\n\t\t\t\t// Snapshot the events-file size BEFORE appending the command:\n\t\t\t\t// any correlated reply is appended strictly after the\n\t\t\t\t// supervisor observes this command, so a tail from this offset\n\t\t\t\t// cannot miss the reply while skipping the entire backlog.\n\t\t\t\tconst fromOffset = yield* Effect.try({\n\t\t\t\t\ttry: () => (existsSync(paths.eventsFile) ? statSync(paths.eventsFile).size : 0),\n\t\t\t\t\tcatch: (cause) =>\n\t\t\t\t\t\tnew CommandChannelIoError({ path: paths.eventsFile, stage: 'stat', cause }),\n\t\t\t\t});\n\t\t\t\tconst record: CommandRecord = {\n\t\t\t\t\tprotocol: COMMAND_CHANNEL_PROTOCOL_VERSION,\n\t\t\t\t\tseq,\n\t\t\t\t\tid,\n\t\t\t\t\tat: Date.now(),\n\t\t\t\t\tpublisherPid: pid,\n\t\t\t\t\tpublisherHostname: host,\n\t\t\t\t\tcommand,\n\t\t\t\t};\n\t\t\t\tyield* appendRecord(paths.commandsFile, record);\n\t\t\t\treturn { id, fromOffset };\n\t\t\t});\n\n\t\tconst decodeEvent = (raw: unknown): EventRecord =>\n\t\t\tdecodeUnknownSync(EventRecordSchema, raw, {\n\t\t\t\tsource: 'command-channel event',\n\t\t\t\tmkError: (issue) => issue,\n\t\t\t});\n\n\t\tconst events: Stream.Stream<EventRecord, CommandChannelError, Scope.Scope> = tailRecords(\n\t\t\tpaths.eventsFile,\n\t\t\t(raw) => decodeEvent(raw),\n\t\t\t{ fromOffset: 'current', onDecodeError: 'skip' },\n\t\t);\n\n\t\tconst findReply = (\n\t\t\tid: string,\n\t\t\tfromOffset: number,\n\t\t): Effect.Effect<\n\t\t\tOption.Option<Extract<EventRecord, { kind: 'ack' | 'error' }>>,\n\t\t\tCommandChannelError,\n\t\t\tScope.Scope\n\t\t> =>\n\t\t\t// Tail from the publish-time offset rather than 'start': the\n\t\t\t// reply is always appended after this point, so scanning the\n\t\t\t// whole file on every await (O(N·total)) is wasteful. The filter\n\t\t\t// by id is the actual correlation.\n\t\t\t// `onDecodeError: 'skip'` keeps the tail alive across truncated /\n\t\t\t// corrupt lines from a peer's mid-flight atomic append, per\n\t\t\t// STYLE_GUIDE §20 NDJSON tolerance rule.\n\t\t\tStream.runHead(\n\t\t\t\ttailRecords(paths.eventsFile, (raw) => decodeEvent(raw), {\n\t\t\t\t\tfromOffset,\n\t\t\t\t\tonDecodeError: 'skip',\n\t\t\t\t}).pipe(\n\t\t\t\t\tStream.filter(\n\t\t\t\t\t\t(rec): rec is Extract<EventRecord, { kind: 'ack' | 'error' }> =>\n\t\t\t\t\t\t\t(rec.kind === 'ack' || rec.kind === 'error') && rec.correlatesTo === id,\n\t\t\t\t\t),\n\t\t\t\t\tStream.take(1),\n\t\t\t\t),\n\t\t\t);\n\n\t\tconst awaitCompletion = (\n\t\t\tpublished: PublishedCommand,\n\t\t\toptions: { readonly timeoutMillis?: number } = {},\n\t\t): Effect.Effect<\n\t\t\t| { readonly ok: true; readonly payload?: unknown }\n\t\t\t| { readonly ok: false; readonly message: string; readonly payload?: unknown },\n\t\t\tCommandChannelError\n\t\t> => {\n\t\t\t// Always bound the wait: a missing reply (supervisor died, dropped\n\t\t\t// record) must NOT park the fiber forever, so an absent\n\t\t\t// `timeoutMillis` falls back to `DEFAULT_AWAIT_COMPLETION_MILLIS`.\n\t\t\tconst timeoutMillis = options.timeoutMillis ?? DEFAULT_AWAIT_COMPLETION_MILLIS;\n\t\t\tconst final = Effect.scoped(findReply(published.id, published.fromOffset)).pipe(\n\t\t\t\tEffect.timeoutOption(`${timeoutMillis} millis`),\n\t\t\t\tEffect.map((outer) => Option.flatten(outer)),\n\t\t\t);\n\t\t\treturn final.pipe(\n\t\t\t\tEffect.map((reply) => {\n\t\t\t\t\tconst inner = Option.getOrNull(reply);\n\t\t\t\t\tif (inner === null) {\n\t\t\t\t\t\treturn { ok: false as const, message: 'timed out waiting for ack' };\n\t\t\t\t\t}\n\t\t\t\t\tif (inner.kind === 'ack') {\n\t\t\t\t\t\treturn inner.payload !== undefined\n\t\t\t\t\t\t\t? { ok: true as const, payload: inner.payload }\n\t\t\t\t\t\t\t: { ok: true as const };\n\t\t\t\t\t}\n\t\t\t\t\treturn {\n\t\t\t\t\t\tok: false as const,\n\t\t\t\t\t\tmessage: inner.detail ? `${inner.message}: ${inner.detail}` : inner.message,\n\t\t\t\t\t\t...(inner.payload !== undefined ? { payload: inner.payload } : {}),\n\t\t\t\t\t};\n\t\t\t\t}),\n\t\t\t);\n\t\t};\n\n\t\treturn { publish, awaitCompletion, events };\n\t});\n\n// -----------------------------------------------------------------------------\n// Supervisor side\n// -----------------------------------------------------------------------------\n\nexport interface CommandChannelSubscriberOptions {\n\t/** From which offset to start reading commands. Defaults to `current`\n\t * so a freshly-booted supervisor doesn't replay queued intents from\n\t * before it was alive. */\n\treadonly fromOffset?: 'start' | 'current';\n\treadonly pollMillis?: number;\n}\n\n/** Tail incoming commands. The supervisor wires its event hub through\n * `publishEvent` so engine events appear on the events file. */\nexport interface CommandChannelSubscriber {\n\treadonly commands: Stream.Stream<CommandRecord, CommandChannelError, Scope.Scope>;\n\treadonly publishEvent: (event: unknown) => Effect.Effect<void, CommandChannelError>;\n\treadonly ack: (\n\t\tcorrelatesTo: string,\n\t\tdetail?: string,\n\t\tpayload?: unknown,\n\t) => Effect.Effect<void, CommandChannelError>;\n\treadonly fail: (\n\t\tcorrelatesTo: string,\n\t\tmessage: string,\n\t\tdetail?: string,\n\t\tpayload?: unknown,\n\t) => Effect.Effect<void, CommandChannelError>;\n}\n\ninterface SubscriberState {\n\treadonly seq: Ref.Ref<number>;\n}\n\nconst nextSubSeq = (state: SubscriberState): Effect.Effect<number> =>\n\tRef.modify(state.seq, (n) => [n + 1, n + 1]);\n\n/** Build the supervisor half. The commands Stream is scope-bound. */\nexport const makeCommandChannelSubscriber = (\n\tpaths: CommandChannelPaths,\n\toptions: CommandChannelSubscriberOptions = {},\n): Effect.Effect<CommandChannelSubscriber, CommandChannelError> =>\n\tEffect.gen(function* () {\n\t\tyield* ensureFile(paths.commandsFile);\n\t\tyield* ensureFile(paths.eventsFile);\n\t\tconst state: SubscriberState = { seq: yield* Ref.make(0) };\n\n\t\tconst decodeCommand = (raw: unknown): CommandRecord =>\n\t\t\tdecodeUnknownSync(CommandRecordSchema, raw, {\n\t\t\t\tsource: 'command-channel command',\n\t\t\t\tmkError: (issue) => issue,\n\t\t\t});\n\n\t\tconst commands: Stream.Stream<CommandRecord, CommandChannelError, Scope.Scope> = tailRecords(\n\t\t\tpaths.commandsFile,\n\t\t\t(raw) => decodeCommand(raw),\n\t\t\t{\n\t\t\t\tfromOffset: options.fromOffset ?? 'current',\n\t\t\t\tpollMillis: options.pollMillis,\n\t\t\t},\n\t\t);\n\n\t\tconst writeEvent = (record: EventRecord): Effect.Effect<void, CommandChannelError> =>\n\t\t\tappendRecord(paths.eventsFile, record);\n\n\t\tconst publishEvent = (event: unknown): Effect.Effect<void, CommandChannelError> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst seq = yield* nextSubSeq(state);\n\t\t\t\tyield* writeEvent({\n\t\t\t\t\tprotocol: COMMAND_CHANNEL_PROTOCOL_VERSION,\n\t\t\t\t\tseq,\n\t\t\t\t\tat: Date.now(),\n\t\t\t\t\tkind: 'engine',\n\t\t\t\t\tevent,\n\t\t\t\t});\n\t\t\t});\n\n\t\tconst ack = (\n\t\t\tcorrelatesTo: string,\n\t\t\tdetail?: string,\n\t\t\tpayload?: unknown,\n\t\t): Effect.Effect<void, CommandChannelError> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst seq = yield* nextSubSeq(state);\n\t\t\t\tyield* writeEvent({\n\t\t\t\t\tprotocol: COMMAND_CHANNEL_PROTOCOL_VERSION,\n\t\t\t\t\tseq,\n\t\t\t\t\tat: Date.now(),\n\t\t\t\t\tkind: 'ack',\n\t\t\t\t\tcorrelatesTo,\n\t\t\t\t\t...(detail !== undefined ? { detail } : {}),\n\t\t\t\t\t...(payload !== undefined ? { payload } : {}),\n\t\t\t\t});\n\t\t\t});\n\n\t\tconst fail = (\n\t\t\tcorrelatesTo: string,\n\t\t\tmessage: string,\n\t\t\tdetail?: string,\n\t\t\tpayload?: unknown,\n\t\t): Effect.Effect<void, CommandChannelError> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst seq = yield* nextSubSeq(state);\n\t\t\t\tyield* writeEvent({\n\t\t\t\t\tprotocol: COMMAND_CHANNEL_PROTOCOL_VERSION,\n\t\t\t\t\tseq,\n\t\t\t\t\tat: Date.now(),\n\t\t\t\t\tkind: 'error',\n\t\t\t\t\tcorrelatesTo,\n\t\t\t\t\tmessage,\n\t\t\t\t\t...(detail !== undefined ? { detail } : {}),\n\t\t\t\t\t...(payload !== undefined ? { payload } : {}),\n\t\t\t\t});\n\t\t\t});\n\n\t\treturn { commands, publishEvent, ack, fail };\n\t});\n"],"mappings":"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"}
1
+ {"version":3,"file":"channel.mjs","names":["nodeHostname"],"sources":["../../../../../src/substrate/runtime/cross-process/command-channel/channel.ts"],"sourcesContent":["// High-level cross-process command channel.\n//\n// Two halves built on the same `commands.ndjson` + `events.ndjson` pair:\n//\n// - Publisher side (CLI / TUI / programmable API caller): appends to\n// `commands.ndjson`; tails `events.ndjson` so it can correlate\n// ack/error replies AND surface incoming engine events (for `logs`\n// and `status --watch`-style verbs).\n//\n// - Supervisor side: tails `commands.ndjson` to receive incoming\n// intents; appends to `events.ndjson` to broadcast engine events\n// and ack/error replies.\n//\n// Both halves are scope-bound — the underlying poll fiber is forked\n// into the surrounding Scope and stops when the Scope closes.\n\nimport { existsSync, statSync } from 'node:fs';\nimport { hostname as nodeHostname } from 'node:os';\nimport { randomUUID } from 'node:crypto';\n\nimport { Effect, Option, Ref, Stream, Scope } from 'effect';\n\nimport type { EngineCommand } from '../../../events.ts';\nimport { decodeUnknownSync } from '../../runtime-decode.ts';\nimport { selfPid } from '../self-pid.ts';\nimport {\n\ttype CommandChannelError,\n\tCommandChannelIoError,\n\tappendRecord,\n\tensureFile,\n\ttailRecords,\n} from './file-channel.ts';\nimport {\n\tCOMMAND_CHANNEL_PROTOCOL_VERSION,\n\tCommandRecordSchema,\n\tEventRecordSchema,\n\ttype CommandRecord,\n\ttype EventRecord,\n} from './protocol.ts';\n\nexport interface CommandChannelPaths {\n\treadonly commandsFile: string;\n\treadonly eventsFile: string;\n}\n\nexport const COMMAND_CHANNEL_COMMANDS_FILE_NAME = 'commands.ndjson';\nexport const COMMAND_CHANNEL_EVENTS_FILE_NAME = 'events.ndjson';\n\n/** Build a `command-channel` paths bundle from a stackRoot dir. The\n * files live next to the existing cross-process artifacts. */\nexport const commandChannelPaths = (stackRoot: string): CommandChannelPaths => ({\n\tcommandsFile: `${stackRoot}/${COMMAND_CHANNEL_COMMANDS_FILE_NAME}`,\n\teventsFile: `${stackRoot}/${COMMAND_CHANNEL_EVENTS_FILE_NAME}`,\n});\n\n// -----------------------------------------------------------------------------\n// Publisher side\n// -----------------------------------------------------------------------------\n\nexport interface PublishedCommand {\n\t/** Per-record id used to correlate `ack` / `error` replies. */\n\treadonly id: string;\n\t/** Byte offset of the events file at publish time. `awaitCompletion`\n\t * tails from here so each await scans only events appended after the\n\t * command was published, instead of replaying the whole file on every\n\t * call (O(N·total) rescans). A reply can only land at-or-after this\n\t * offset because the supervisor reads the command before acking. */\n\treadonly fromOffset: number;\n}\n\n/** Default bound for `awaitCompletion` when the caller passes no\n * `timeoutMillis`. A correlated ack/error normally lands within tens of\n * ms; an absent reply (supervisor died mid-handle, dropped record) must\n * NOT park the await fiber forever, so the wait is always bounded. */\nexport const DEFAULT_AWAIT_COMPLETION_MILLIS = 30_000;\n\n/**\n * Publisher half: append a typed command, optionally await its\n * corresponding ack/error reply.\n *\n * `publish(cmd)` writes one record and returns the id; the caller can\n * then `awaitCompletion(id)` to block until the supervisor either acks\n * or errors. `awaitCompletion` is a separate seam so verbs that don't\n * need correlation (TUI-style fire-and-forget) skip the wait.\n */\nexport interface CommandChannelPublisher {\n\treadonly publish: (\n\t\tcommand: EngineCommand,\n\t) => Effect.Effect<PublishedCommand, CommandChannelError>;\n\treadonly awaitCompletion: (\n\t\tpublished: PublishedCommand,\n\t\toptions?: { readonly timeoutMillis?: number },\n\t) => Effect.Effect<\n\t\t| { readonly ok: true; readonly payload?: unknown }\n\t\t| { readonly ok: false; readonly message: string; readonly payload?: unknown },\n\t\tCommandChannelError\n\t>;\n\treadonly events: Stream.Stream<EventRecord, CommandChannelError, Scope.Scope>;\n}\n\ninterface PublisherState {\n\treadonly seq: Ref.Ref<number>;\n}\n\nconst nextSeq = (state: PublisherState): Effect.Effect<number> =>\n\tRef.modify(state.seq, (n) => [n + 1, n + 1]);\n\n/**\n * Build the publisher half. The events Stream is scope-bound:\n * pass it through `Stream.runDrain` inside a `Scope.use` or fork it\n * with `Effect.forkScoped` so the tail polling cleans up.\n */\nexport const makeCommandChannelPublisher = (\n\tpaths: CommandChannelPaths,\n): Effect.Effect<CommandChannelPublisher, CommandChannelError> =>\n\tEffect.gen(function* () {\n\t\tyield* ensureFile(paths.commandsFile);\n\t\tyield* ensureFile(paths.eventsFile);\n\t\tconst state: PublisherState = { seq: yield* Ref.make(0) };\n\t\tconst pid = selfPid();\n\t\tconst host = nodeHostname();\n\n\t\tconst publish = (\n\t\t\tcommand: EngineCommand,\n\t\t): Effect.Effect<PublishedCommand, CommandChannelError> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst seq = yield* nextSeq(state);\n\t\t\t\tconst id = `${pid}-${seq}-${randomUUID().slice(0, 8)}`;\n\t\t\t\t// Snapshot the events-file size BEFORE appending the command:\n\t\t\t\t// any correlated reply is appended strictly after the\n\t\t\t\t// supervisor observes this command, so a tail from this offset\n\t\t\t\t// cannot miss the reply while skipping the entire backlog.\n\t\t\t\tconst fromOffset = yield* Effect.try({\n\t\t\t\t\ttry: () => (existsSync(paths.eventsFile) ? statSync(paths.eventsFile).size : 0),\n\t\t\t\t\tcatch: (cause) =>\n\t\t\t\t\t\tnew CommandChannelIoError({ path: paths.eventsFile, stage: 'stat', cause }),\n\t\t\t\t});\n\t\t\t\tconst record: CommandRecord = {\n\t\t\t\t\tprotocol: COMMAND_CHANNEL_PROTOCOL_VERSION,\n\t\t\t\t\tseq,\n\t\t\t\t\tid,\n\t\t\t\t\tat: Date.now(),\n\t\t\t\t\tpublisherPid: pid,\n\t\t\t\t\tpublisherHostname: host,\n\t\t\t\t\tcommand,\n\t\t\t\t};\n\t\t\t\tyield* appendRecord(paths.commandsFile, record);\n\t\t\t\treturn { id, fromOffset };\n\t\t\t});\n\n\t\tconst decodeEvent = (raw: unknown): EventRecord =>\n\t\t\tdecodeUnknownSync(EventRecordSchema, raw, {\n\t\t\t\tsource: 'command-channel event',\n\t\t\t\tmkError: (issue) => issue,\n\t\t\t});\n\n\t\tconst events: Stream.Stream<EventRecord, CommandChannelError, Scope.Scope> = tailRecords(\n\t\t\tpaths.eventsFile,\n\t\t\t(raw) => decodeEvent(raw),\n\t\t\t{ fromOffset: 'current', onDecodeError: 'skip' },\n\t\t);\n\n\t\tconst findReply = (\n\t\t\tid: string,\n\t\t\tfromOffset: number,\n\t\t): Effect.Effect<\n\t\t\tOption.Option<Extract<EventRecord, { kind: 'ack' | 'error' }>>,\n\t\t\tCommandChannelError,\n\t\t\tScope.Scope\n\t\t> =>\n\t\t\t// Tail from the publish-time offset rather than 'start': the\n\t\t\t// reply is always appended after this point, so scanning the\n\t\t\t// whole file on every await (O(N·total)) is wasteful. The filter\n\t\t\t// by id is the actual correlation.\n\t\t\t// `onDecodeError: 'skip'` keeps the tail alive across truncated /\n\t\t\t// corrupt lines from a peer's mid-flight atomic append, per\n\t\t\t// STYLE_GUIDE §20 NDJSON tolerance rule.\n\t\t\tStream.runHead(\n\t\t\t\ttailRecords(paths.eventsFile, (raw) => decodeEvent(raw), {\n\t\t\t\t\tfromOffset,\n\t\t\t\t\tonDecodeError: 'skip',\n\t\t\t\t}).pipe(\n\t\t\t\t\tStream.filter(\n\t\t\t\t\t\t(rec): rec is Extract<EventRecord, { kind: 'ack' | 'error' }> =>\n\t\t\t\t\t\t\t(rec.kind === 'ack' || rec.kind === 'error') && rec.correlatesTo === id,\n\t\t\t\t\t),\n\t\t\t\t\tStream.take(1),\n\t\t\t\t),\n\t\t\t);\n\n\t\tconst awaitCompletion = (\n\t\t\tpublished: PublishedCommand,\n\t\t\toptions: { readonly timeoutMillis?: number } = {},\n\t\t): Effect.Effect<\n\t\t\t| { readonly ok: true; readonly payload?: unknown }\n\t\t\t| { readonly ok: false; readonly message: string; readonly payload?: unknown },\n\t\t\tCommandChannelError\n\t\t> => {\n\t\t\t// Always bound the wait: a missing reply (supervisor died, dropped\n\t\t\t// record) must NOT park the fiber forever, so an absent\n\t\t\t// `timeoutMillis` falls back to `DEFAULT_AWAIT_COMPLETION_MILLIS`.\n\t\t\tconst timeoutMillis = options.timeoutMillis ?? DEFAULT_AWAIT_COMPLETION_MILLIS;\n\t\t\tconst final = Effect.scoped(findReply(published.id, published.fromOffset)).pipe(\n\t\t\t\tEffect.timeoutOption(`${timeoutMillis} millis`),\n\t\t\t\tEffect.map((outer) => Option.flatten(outer)),\n\t\t\t);\n\t\t\treturn final.pipe(\n\t\t\t\tEffect.map((reply) => {\n\t\t\t\t\tconst inner = Option.getOrNull(reply);\n\t\t\t\t\tif (inner === null) {\n\t\t\t\t\t\treturn { ok: false as const, message: 'timed out waiting for ack' };\n\t\t\t\t\t}\n\t\t\t\t\tif (inner.kind === 'ack') {\n\t\t\t\t\t\treturn inner.payload !== undefined\n\t\t\t\t\t\t\t? { ok: true as const, payload: inner.payload }\n\t\t\t\t\t\t\t: { ok: true as const };\n\t\t\t\t\t}\n\t\t\t\t\treturn {\n\t\t\t\t\t\tok: false as const,\n\t\t\t\t\t\tmessage: inner.detail ? `${inner.message}: ${inner.detail}` : inner.message,\n\t\t\t\t\t\t...(inner.payload !== undefined ? { payload: inner.payload } : {}),\n\t\t\t\t\t};\n\t\t\t\t}),\n\t\t\t);\n\t\t};\n\n\t\treturn { publish, awaitCompletion, events };\n\t});\n\n// -----------------------------------------------------------------------------\n// Supervisor side\n// -----------------------------------------------------------------------------\n\nexport interface CommandChannelSubscriberOptions {\n\t/** From which offset to start reading commands. Defaults to `current`\n\t * so a freshly-booted supervisor doesn't replay queued intents from\n\t * before it was alive. */\n\treadonly fromOffset?: 'start' | 'current';\n\treadonly pollMillis?: number;\n}\n\n/** One correlated reply to a published command. `ack` carries an\n * optional `detail` + structured `payload`; `error` adds the required\n * human `message`. The two share `publishReply` (below) — the wire\n * `EventRecord` schema is unchanged (kind `'ack'` / `'error'`). */\nexport type CommandReply =\n\t| { readonly kind: 'ack'; readonly detail?: string; readonly payload?: unknown }\n\t| {\n\t\t\treadonly kind: 'error';\n\t\t\treadonly message: string;\n\t\t\treadonly detail?: string;\n\t\t\treadonly payload?: unknown;\n\t };\n\n/** Tail incoming commands. The supervisor wires its event hub through\n * `publishEvent` so engine events appear on the events file. */\nexport interface CommandChannelSubscriber {\n\treadonly commands: Stream.Stream<CommandRecord, CommandChannelError, Scope.Scope>;\n\treadonly publishEvent: (event: unknown) => Effect.Effect<void, CommandChannelError>;\n\t/** Append a correlated `ack` / `error` reply for command `correlatesTo`.\n\t * Collapses the former `ack(...)` / `fail(...)` pair into one verb. */\n\treadonly publishReply: (\n\t\tcorrelatesTo: string,\n\t\treply: CommandReply,\n\t) => Effect.Effect<void, CommandChannelError>;\n}\n\ninterface SubscriberState {\n\treadonly seq: Ref.Ref<number>;\n}\n\nconst nextSubSeq = (state: SubscriberState): Effect.Effect<number> =>\n\tRef.modify(state.seq, (n) => [n + 1, n + 1]);\n\n/** Build the supervisor half. The commands Stream is scope-bound. */\nexport const makeCommandChannelSubscriber = (\n\tpaths: CommandChannelPaths,\n\toptions: CommandChannelSubscriberOptions = {},\n): Effect.Effect<CommandChannelSubscriber, CommandChannelError> =>\n\tEffect.gen(function* () {\n\t\tyield* ensureFile(paths.commandsFile);\n\t\tyield* ensureFile(paths.eventsFile);\n\t\tconst state: SubscriberState = { seq: yield* Ref.make(0) };\n\n\t\tconst decodeCommand = (raw: unknown): CommandRecord =>\n\t\t\tdecodeUnknownSync(CommandRecordSchema, raw, {\n\t\t\t\tsource: 'command-channel command',\n\t\t\t\tmkError: (issue) => issue,\n\t\t\t});\n\n\t\tconst commands: Stream.Stream<CommandRecord, CommandChannelError, Scope.Scope> = tailRecords(\n\t\t\tpaths.commandsFile,\n\t\t\t(raw) => decodeCommand(raw),\n\t\t\t{\n\t\t\t\tfromOffset: options.fromOffset ?? 'current',\n\t\t\t\tpollMillis: options.pollMillis,\n\t\t\t},\n\t\t);\n\n\t\tconst writeEvent = (record: EventRecord): Effect.Effect<void, CommandChannelError> =>\n\t\t\tappendRecord(paths.eventsFile, record);\n\n\t\tconst publishEvent = (event: unknown): Effect.Effect<void, CommandChannelError> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst seq = yield* nextSubSeq(state);\n\t\t\t\tyield* writeEvent({\n\t\t\t\t\tprotocol: COMMAND_CHANNEL_PROTOCOL_VERSION,\n\t\t\t\t\tseq,\n\t\t\t\t\tat: Date.now(),\n\t\t\t\t\tkind: 'engine',\n\t\t\t\t\tevent,\n\t\t\t\t});\n\t\t\t});\n\n\t\tconst publishReply = (\n\t\t\tcorrelatesTo: string,\n\t\t\treply: CommandReply,\n\t\t): Effect.Effect<void, CommandChannelError> =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst seq = yield* nextSubSeq(state);\n\t\t\t\tyield* writeEvent(\n\t\t\t\t\treply.kind === 'ack'\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\tprotocol: COMMAND_CHANNEL_PROTOCOL_VERSION,\n\t\t\t\t\t\t\t\tseq,\n\t\t\t\t\t\t\t\tat: Date.now(),\n\t\t\t\t\t\t\t\tkind: 'ack',\n\t\t\t\t\t\t\t\tcorrelatesTo,\n\t\t\t\t\t\t\t\t...(reply.detail !== undefined ? { detail: reply.detail } : {}),\n\t\t\t\t\t\t\t\t...(reply.payload !== undefined ? { payload: reply.payload } : {}),\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\tprotocol: COMMAND_CHANNEL_PROTOCOL_VERSION,\n\t\t\t\t\t\t\t\tseq,\n\t\t\t\t\t\t\t\tat: Date.now(),\n\t\t\t\t\t\t\t\tkind: 'error',\n\t\t\t\t\t\t\t\tcorrelatesTo,\n\t\t\t\t\t\t\t\tmessage: reply.message,\n\t\t\t\t\t\t\t\t...(reply.detail !== undefined ? { detail: reply.detail } : {}),\n\t\t\t\t\t\t\t\t...(reply.payload !== undefined ? { payload: reply.payload } : {}),\n\t\t\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t});\n\n\t\treturn { commands, publishEvent, publishReply };\n\t});\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"file-channel.mjs","names":[],"sources":["../../../../../src/substrate/runtime/cross-process/command-channel/file-channel.ts"],"sourcesContent":["// Append-only NDJSON channel.\n//\n// One-record-per-line. Each append is `fs.appendFileSync` with\n// `flag: 'a'`, serialized cross-process by the runtime-control lock\n// (`withRuntimeControlLock`, an O_EXCL create/unlink). The LOCK — not\n// PIPE_BUF — is what guarantees a record never interleaves with a\n// concurrent append: a JSON record can exceed PIPE_BUF (the POSIX\n// atomic-append bound is only ~512 bytes on macOS, 4096 on Linux), so\n// relying on PIPE_BUF alone would let a large record tear under\n// contention. The tail-reader polls via offset bookkeeping — `fs.watch`\n// is unreliable across platforms for \"file grew\" notifications\n// (especially over NFS, which the cross-process protocol must remain\n// safe on per architecture § Cross-process safety protocol).\n//\n// Records are framed by a literal newline. A partial trailing line\n// (writer mid-flight when reader observed, or a short read bisecting a\n// record) is buffered as raw bytes until a later poll completes the\n// line — see `drainNewLines` for why the buffer is bytes, not a string.\n\nimport {\n\tappendFileSync,\n\tcloseSync,\n\texistsSync,\n\tmkdirSync,\n\topenSync,\n\treadSync,\n\tstatSync,\n\twriteFileSync,\n} from 'node:fs';\nimport { dirname } from 'node:path';\n\nimport { Data, Effect, Scope, Stream } from 'effect';\n\nimport { parseJsonText } from '../../runtime-decode.ts';\nimport { acquireStackLock } from '../stack-lock.ts';\nimport { runtimeControlLockPathForChannelFile } from './runtime-control-lock.ts';\n\n/** Tagged failure for any I/O error during append. */\nexport class CommandChannelIoError extends Data.TaggedError('CommandChannelIoError')<{\n\treadonly path: string;\n\treadonly stage: 'append' | 'open' | 'read' | 'stat' | 'init';\n\treadonly cause: unknown;\n}> {}\n\n/** Tagged failure when a tailed line cannot be decoded against the\n * record schema. The malformed line is preserved so consumers can\n * surface it for debugging without spamming retries. */\nexport class CommandChannelDecodeError extends Data.TaggedError('CommandChannelDecodeError')<{\n\treadonly path: string;\n\treadonly line: string;\n\treadonly cause: unknown;\n}> {}\n\nexport type CommandChannelError = CommandChannelIoError | CommandChannelDecodeError;\n\n/** Default tail poll interval. Chosen to be small enough for\n * interactive CLI responsiveness (a `down` ack returns within ~50ms)\n * while staying out of the way of process schedulers. */\nexport const DEFAULT_TAIL_POLL_MILLIS = 50;\n\nconst withRuntimeControlLock = <A>(\n\tpath: string,\n\tstage: CommandChannelIoError['stage'],\n\teffect: Effect.Effect<A, CommandChannelIoError>,\n): Effect.Effect<A, CommandChannelIoError> =>\n\tEffect.scoped(\n\t\tEffect.gen(function* () {\n\t\t\tyield* acquireStackLock(runtimeControlLockPathForChannelFile(path)).pipe(\n\t\t\t\tEffect.mapError((cause) => new CommandChannelIoError({ path, stage, cause })),\n\t\t\t);\n\t\t\treturn yield* effect;\n\t\t}),\n\t);\n\n/** Ensure the file exists. Idempotent. Creates parent dir if needed. */\nexport const ensureFile = (path: string): Effect.Effect<void, CommandChannelIoError> =>\n\twithRuntimeControlLock(\n\t\tpath,\n\t\t'init',\n\t\tEffect.try({\n\t\t\ttry: () => {\n\t\t\t\tmkdirSync(dirname(path), { recursive: true });\n\t\t\t\tif (!existsSync(path)) {\n\t\t\t\t\twriteFileSync(path, '', { flag: 'a' });\n\t\t\t\t}\n\t\t\t},\n\t\t\tcatch: (cause) => new CommandChannelIoError({ path, stage: 'init', cause }),\n\t\t}),\n\t);\n\n/** Append one record as a single NDJSON line. The trailing newline\n * ensures the next writer's append starts a fresh record. */\nexport const appendRecord = (\n\tpath: string,\n\trecord: unknown,\n): Effect.Effect<void, CommandChannelIoError> =>\n\twithRuntimeControlLock(\n\t\tpath,\n\t\t'append',\n\t\tEffect.try({\n\t\t\ttry: () => {\n\t\t\t\tmkdirSync(dirname(path), { recursive: true });\n\t\t\t\tappendFileSync(path, `${JSON.stringify(record)}\\n`, { flag: 'a' });\n\t\t\t},\n\t\t\tcatch: (cause) => new CommandChannelIoError({ path, stage: 'append', cause }),\n\t\t}),\n\t);\n\n/** A line decoder. The channel I/O layer stays schema-agnostic so\n * callers can pick their own wire schema without this generic\n * carrying a `Schema.Decoder<unknown>` constraint. */\nexport type LineDecoder<A> = (raw: unknown) => A;\n\nconst decodeLine = <A>(\n\tpath: string,\n\tline: string,\n\tdecode: LineDecoder<A>,\n): Effect.Effect<A, CommandChannelDecodeError> =>\n\tparseJsonText(line, {\n\t\tsource: path,\n\t\tmkError: (issue) => new CommandChannelDecodeError({ path, line, cause: issue.cause ?? issue }),\n\t}).pipe(\n\t\tEffect.flatMap((parsed) =>\n\t\t\tEffect.try({\n\t\t\t\ttry: () => decode(parsed),\n\t\t\t\tcatch: (cause) => new CommandChannelDecodeError({ path, line, cause }),\n\t\t\t}),\n\t\t),\n\t);\n\n/** Read the entire current contents into structured records. Useful at\n * startup to backfill state, and as a test seam. */\nexport const readAllRecords = <A>(\n\tpath: string,\n\tdecode: LineDecoder<A>,\n): Effect.Effect<ReadonlyArray<A>, CommandChannelError> =>\n\tEffect.gen(function* () {\n\t\tif (!existsSync(path)) return [];\n\t\tconst raw = yield* Effect.try({\n\t\t\ttry: () => {\n\t\t\t\tconst fd = openSync(path, 'r');\n\t\t\t\ttry {\n\t\t\t\t\tconst { size } = statSync(path);\n\t\t\t\t\tconst buf = Buffer.alloc(size);\n\t\t\t\t\treadSync(fd, buf, 0, size, 0);\n\t\t\t\t\treturn buf.toString('utf8');\n\t\t\t\t} finally {\n\t\t\t\t\tcloseSync(fd);\n\t\t\t\t}\n\t\t\t},\n\t\t\tcatch: (cause) => new CommandChannelIoError({ path, stage: 'read', cause }),\n\t\t});\n\t\tconst lines = raw.split('\\n').filter((l) => l.length > 0);\n\t\tconst out: A[] = [];\n\t\tfor (const line of lines) {\n\t\t\tout.push(yield* decodeLine(path, line, decode));\n\t\t}\n\t\treturn out;\n\t});\n\ninterface TailState {\n\toffset: number;\n\t/** Raw, not-yet-decoded bytes of the trailing partial line. Held as a\n\t * Buffer rather than a decoded string so an incomplete UTF-8\n\t * multibyte sequence straddling a short-read boundary survives intact\n\t * until its continuation bytes arrive on the next poll. Decoding\n\t * per-read (the old behaviour) emitted U+FFFD for the split sequence\n\t * and lost the continuation bytes, which could corrupt a non-ASCII\n\t * record into invalid JSON that is then silently dropped. */\n\tpartial: Buffer;\n}\n\n/** Read newly-appended bytes from `state.offset` to current EOF. Updates\n * `state` in place. Returns the freshly-completed lines. A trailing\n * partial line (no newline yet) is preserved as raw bytes in\n * `state.partial` and prepended on the next poll. */\nconst drainNewLines = (\n\tpath: string,\n\tstate: TailState,\n): Effect.Effect<ReadonlyArray<string>, CommandChannelIoError> =>\n\tEffect.try({\n\t\ttry: () => {\n\t\t\tif (!existsSync(path)) return [];\n\t\t\tconst stat = statSync(path);\n\t\t\tif (stat.size <= state.offset) return [];\n\t\t\tconst grow = stat.size - state.offset;\n\t\t\tconst fd = openSync(path, 'r');\n\t\t\ttry {\n\t\t\t\tconst buf = Buffer.alloc(grow);\n\t\t\t\t// POSIX `read(2)` may short-return (especially across\n\t\t\t\t// filesystems like NFS); advance the offset by what was\n\t\t\t\t// ACTUALLY read, not what we asked for. A short read just\n\t\t\t\t// means the rest will arrive on the next poll iteration.\n\t\t\t\tconst bytesRead = readSync(fd, buf, 0, grow, state.offset);\n\t\t\t\tif (bytesRead <= 0) return [];\n\t\t\t\tstate.offset += bytesRead;\n\t\t\t\t// Split on the newline BYTE (0x0A) and decode only complete\n\t\t\t\t// lines. A short read can bisect a multibyte codepoint; by\n\t\t\t\t// keeping the trailing fragment as raw bytes we never decode\n\t\t\t\t// a half codepoint, so non-ASCII records reassemble byte-for-\n\t\t\t\t// byte across poll cycles. 0x0A never appears as a\n\t\t\t\t// continuation byte in UTF-8, so byte-level framing is sound.\n\t\t\t\tconst chunk = Buffer.concat([state.partial, buf.subarray(0, bytesRead)]);\n\t\t\t\tconst completedLines: string[] = [];\n\t\t\t\tlet lineStart = 0;\n\t\t\t\tfor (let i = 0; i < chunk.length; i++) {\n\t\t\t\t\tif (chunk[i] === 0x0a) {\n\t\t\t\t\t\tif (i > lineStart) {\n\t\t\t\t\t\t\tcompletedLines.push(chunk.toString('utf8', lineStart, i));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlineStart = i + 1;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// Retain the unterminated remainder (possibly an incomplete\n\t\t\t\t// multibyte sequence) as raw bytes for the next poll.\n\t\t\t\tstate.partial = Buffer.from(chunk.subarray(lineStart));\n\t\t\t\treturn completedLines;\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t},\n\t\tcatch: (cause) => new CommandChannelIoError({ path, stage: 'read', cause }),\n\t});\n\n/** Tail a file as a Stream of decoded records. The Stream is bound to\n * the surrounding Scope: when the Scope closes, polling stops.\n *\n * - `fromOffset === 'current'` skips existing content; starts at EOF.\n * - `fromOffset === 'start'` replays every existing record then tails.\n * - `fromOffset === number` resumes from a known byte offset.\n *\n * By default decode errors are surfaced as Stream failures with the\n * offending line attached so the consumer can pick a retry / skip\n * policy. Pass `onDecodeError: 'skip'` to drop the bad line instead —\n * required for NDJSON readers that race atomic-append writers (a\n * truncated/corrupt mid-flight line is normal and MUST NOT kill the\n * stream; see STYLE_GUIDE §20).\n *\n * The poll loop uses `Effect.sleep` between iterations, so cooperative\n * scheduling is preserved (no busy loop).\n */\nexport const tailRecords = <A>(\n\tpath: string,\n\tdecode: LineDecoder<A>,\n\toptions: {\n\t\treadonly fromOffset?: 'start' | 'current' | number;\n\t\treadonly pollMillis?: number;\n\t\treadonly onDecodeError?: 'fail' | 'skip';\n\t} = {},\n): Stream.Stream<A, CommandChannelError, Scope.Scope> =>\n\tStream.unwrap(\n\t\tEffect.gen(function* () {\n\t\t\tconst pollMillis = options.pollMillis ?? DEFAULT_TAIL_POLL_MILLIS;\n\t\t\tconst onDecodeError = options.onDecodeError ?? 'fail';\n\t\t\tyield* ensureFile(path);\n\t\t\tconst initialOffset = yield* Effect.try({\n\t\t\t\ttry: () => {\n\t\t\t\t\tif (options.fromOffset === 'start' || options.fromOffset === undefined) {\n\t\t\t\t\t\treturn 0;\n\t\t\t\t\t}\n\t\t\t\t\tif (options.fromOffset === 'current') {\n\t\t\t\t\t\treturn existsSync(path) ? statSync(path).size : 0;\n\t\t\t\t\t}\n\t\t\t\t\treturn options.fromOffset;\n\t\t\t\t},\n\t\t\t\tcatch: (cause) => new CommandChannelIoError({ path, stage: 'stat', cause }),\n\t\t\t});\n\t\t\tconst state: TailState = { offset: initialOffset, partial: Buffer.alloc(0) };\n\t\t\tconst pollOne: Effect.Effect<ReadonlyArray<A>, CommandChannelError> = Effect.gen(\n\t\t\t\tfunction* () {\n\t\t\t\t\tconst lines = yield* drainNewLines(path, state);\n\t\t\t\t\tif (lines.length === 0) {\n\t\t\t\t\t\tyield* Effect.sleep(`${pollMillis} millis`);\n\t\t\t\t\t\treturn [] as ReadonlyArray<A>;\n\t\t\t\t\t}\n\t\t\t\t\tconst decoded: A[] = [];\n\t\t\t\t\tfor (const line of lines) {\n\t\t\t\t\t\tif (onDecodeError === 'skip') {\n\t\t\t\t\t\t\tconst result = yield* decodeLine(path, line, decode).pipe(\n\t\t\t\t\t\t\t\tEffect.tapError((cause) =>\n\t\t\t\t\t\t\t\t\tEffect.logDebug(`tail-records: skipped malformed line at ${path}`).pipe(\n\t\t\t\t\t\t\t\t\t\tEffect.annotateLogs({ line: cause.line, cause: String(cause.cause) }),\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\tEffect.option,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tif (result._tag === 'Some') {\n\t\t\t\t\t\t\t\tdecoded.push(result.value);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tdecoded.push(yield* decodeLine(path, line, decode));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn decoded as ReadonlyArray<A>;\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn Stream.fromEffectRepeat(pollOne).pipe(\n\t\t\t\tStream.flatMap((records) => Stream.fromIterable(records)),\n\t\t\t);\n\t\t}),\n\t);\n"],"mappings":";;;;;;;;AAsCA,IAAa,wBAAb,cAA2C,KAAK,YAAY,wBAAwB,CAIjF;;;;AAKH,IAAa,4BAAb,cAA+C,KAAK,YAAY,4BAA4B,CAIzF;AASH,MAAM,0BACL,MACA,OACA,WAEA,OAAO,OACN,OAAO,IAAI,aAAa;AACvB,QAAO,iBAAiB,qCAAqC,KAAK,CAAC,CAAC,KACnE,OAAO,UAAU,UAAU,IAAI,sBAAsB;EAAE;EAAM;EAAO;EAAO,CAAC,CAAC,CAC7E;AACD,QAAO,OAAO;EACb,CACF;;AAGF,MAAa,cAAc,SAC1B,uBACC,MACA,QACA,OAAO,IAAI;CACV,WAAW;AACV,YAAU,QAAQ,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;AAC7C,MAAI,CAAC,WAAW,KAAK,CACpB,eAAc,MAAM,IAAI,EAAE,MAAM,KAAK,CAAC;;CAGxC,QAAQ,UAAU,IAAI,sBAAsB;EAAE;EAAM,OAAO;EAAQ;EAAO,CAAC;CAC3E,CAAC,CACF;;;AAIF,MAAa,gBACZ,MACA,WAEA,uBACC,MACA,UACA,OAAO,IAAI;CACV,WAAW;AACV,YAAU,QAAQ,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;AAC7C,iBAAe,MAAM,GAAG,KAAK,UAAU,OAAO,CAAC,KAAK,EAAE,MAAM,KAAK,CAAC;;CAEnE,QAAQ,UAAU,IAAI,sBAAsB;EAAE;EAAM,OAAO;EAAU;EAAO,CAAC;CAC7E,CAAC,CACF;AAOF,MAAM,cACL,MACA,MACA,WAEA,cAAc,MAAM;CACnB,QAAQ;CACR,UAAU,UAAU,IAAI,0BAA0B;EAAE;EAAM;EAAM,OAAO,MAAM,SAAS;EAAO,CAAC;CAC9F,CAAC,CAAC,KACF,OAAO,SAAS,WACf,OAAO,IAAI;CACV,WAAW,OAAO,OAAO;CACzB,QAAQ,UAAU,IAAI,0BAA0B;EAAE;EAAM;EAAM;EAAO,CAAC;CACtE,CAAC,CACF,CACD;;;;;AAgDF,MAAM,iBACL,MACA,UAEA,OAAO,IAAI;CACV,WAAW;AACV,MAAI,CAAC,WAAW,KAAK,CAAE,QAAO,EAAE;EAChC,MAAM,OAAO,SAAS,KAAK;AAC3B,MAAI,KAAK,QAAQ,MAAM,OAAQ,QAAO,EAAE;EACxC,MAAM,OAAO,KAAK,OAAO,MAAM;EAC/B,MAAM,KAAK,SAAS,MAAM,IAAI;AAC9B,MAAI;GACH,MAAM,MAAM,OAAO,MAAM,KAAK;GAK9B,MAAM,YAAY,SAAS,IAAI,KAAK,GAAG,MAAM,MAAM,OAAO;AAC1D,OAAI,aAAa,EAAG,QAAO,EAAE;AAC7B,SAAM,UAAU;GAOhB,MAAM,QAAQ,OAAO,OAAO,CAAC,MAAM,SAAS,IAAI,SAAS,GAAG,UAAU,CAAC,CAAC;GACxE,MAAM,iBAA2B,EAAE;GACnC,IAAI,YAAY;AAChB,QAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,IACjC,KAAI,MAAM,OAAO,IAAM;AACtB,QAAI,IAAI,UACP,gBAAe,KAAK,MAAM,SAAS,QAAQ,WAAW,EAAE,CAAC;AAE1D,gBAAY,IAAI;;AAKlB,SAAM,UAAU,OAAO,KAAK,MAAM,SAAS,UAAU,CAAC;AACtD,UAAO;YACE;AACT,aAAU,GAAG;;;CAGf,QAAQ,UAAU,IAAI,sBAAsB;EAAE;EAAM,OAAO;EAAQ;EAAO,CAAC;CAC3E,CAAC;;;;;;;;;;;;;;;;;;AAmBH,MAAa,eACZ,MACA,QACA,UAII,EAAE,KAEN,OAAO,OACN,OAAO,IAAI,aAAa;CACvB,MAAM,aAAa,QAAQ,cAAA;CAC3B,MAAM,gBAAgB,QAAQ,iBAAiB;AAC/C,QAAO,WAAW,KAAK;CAavB,MAAM,QAAmB;EAAE,QAAQ,OAZN,OAAO,IAAI;GACvC,WAAW;AACV,QAAI,QAAQ,eAAe,WAAW,QAAQ,eAAe,KAAA,EAC5D,QAAO;AAER,QAAI,QAAQ,eAAe,UAC1B,QAAO,WAAW,KAAK,GAAG,SAAS,KAAK,CAAC,OAAO;AAEjD,WAAO,QAAQ;;GAEhB,QAAQ,UAAU,IAAI,sBAAsB;IAAE;IAAM,OAAO;IAAQ;IAAO,CAAC;GAC3E,CAAC;EACgD,SAAS,OAAO,MAAM,EAAE;EAAE;CAC5E,MAAM,UAAgE,OAAO,IAC5E,aAAa;EACZ,MAAM,QAAQ,OAAO,cAAc,MAAM,MAAM;AAC/C,MAAI,MAAM,WAAW,GAAG;AACvB,UAAO,OAAO,MAAM,GAAG,WAAW,SAAS;AAC3C,UAAO,EAAE;;EAEV,MAAM,UAAe,EAAE;AACvB,OAAK,MAAM,QAAQ,MAClB,KAAI,kBAAkB,QAAQ;GAC7B,MAAM,SAAS,OAAO,WAAW,MAAM,MAAM,OAAO,CAAC,KACpD,OAAO,UAAU,UAChB,OAAO,SAAS,2CAA2C,OAAO,CAAC,KAClE,OAAO,aAAa;IAAE,MAAM,MAAM;IAAM,OAAO,OAAO,MAAM,MAAM;IAAE,CAAC,CACrE,CACD,EACD,OAAO,OACP;AACD,OAAI,OAAO,SAAS,OACnB,SAAQ,KAAK,OAAO,MAAM;QAG3B,SAAQ,KAAK,OAAO,WAAW,MAAM,MAAM,OAAO,CAAC;AAGrD,SAAO;GAER;AACD,QAAO,OAAO,iBAAiB,QAAQ,CAAC,KACvC,OAAO,SAAS,YAAY,OAAO,aAAa,QAAQ,CAAC,CACzD;EACA,CACF"}
1
+ {"version":3,"file":"file-channel.mjs","names":[],"sources":["../../../../../src/substrate/runtime/cross-process/command-channel/file-channel.ts"],"sourcesContent":["// Append-only NDJSON channel.\n//\n// One-record-per-line. Each append is `fs.appendFileSync` with\n// `flag: 'a'`, serialized cross-process by the runtime-control lock\n// (`withRuntimeControlLock`, an O_EXCL create/unlink). The LOCK — not\n// PIPE_BUF — is what guarantees a record never interleaves with a\n// concurrent append: a JSON record can exceed PIPE_BUF (the POSIX\n// atomic-append bound is only ~512 bytes on macOS, 4096 on Linux), so\n// relying on PIPE_BUF alone would let a large record tear under\n// contention. The tail-reader polls via offset bookkeeping — `fs.watch`\n// is unreliable across platforms for \"file grew\" notifications, so the\n// cross-process protocol polls rather than relying on it.\n//\n// Records are framed by a literal newline. A partial trailing line\n// (writer mid-flight when reader observed, or a short read bisecting a\n// record) is buffered as raw bytes until a later poll completes the\n// line — see `drainNewLines` for why the buffer is bytes, not a string.\n\nimport {\n\tappendFileSync,\n\tcloseSync,\n\texistsSync,\n\tmkdirSync,\n\topenSync,\n\treadSync,\n\tstatSync,\n\twriteFileSync,\n} from 'node:fs';\nimport { dirname } from 'node:path';\n\nimport { Data, Effect, Scope, Stream } from 'effect';\n\nimport { parseJsonText } from '../../runtime-decode.ts';\nimport { acquireStackLock } from '../stack-lock.ts';\nimport { runtimeControlLockPathForChannelFile } from './runtime-control-lock.ts';\n\n/** Tagged failure for any I/O error during append. */\nexport class CommandChannelIoError extends Data.TaggedError('CommandChannelIoError')<{\n\treadonly path: string;\n\treadonly stage: 'append' | 'open' | 'read' | 'stat' | 'init';\n\treadonly cause: unknown;\n}> {}\n\n/** Tagged failure when a tailed line cannot be decoded against the\n * record schema. The malformed line is preserved so consumers can\n * surface it for debugging without spamming retries. */\nexport class CommandChannelDecodeError extends Data.TaggedError('CommandChannelDecodeError')<{\n\treadonly path: string;\n\treadonly line: string;\n\treadonly cause: unknown;\n}> {}\n\nexport type CommandChannelError = CommandChannelIoError | CommandChannelDecodeError;\n\n/** Default tail poll interval. Chosen to be small enough for\n * interactive CLI responsiveness (a `down` ack returns within ~50ms)\n * while staying out of the way of process schedulers. */\nexport const DEFAULT_TAIL_POLL_MILLIS = 50;\n\nconst withRuntimeControlLock = <A>(\n\tpath: string,\n\tstage: CommandChannelIoError['stage'],\n\teffect: Effect.Effect<A, CommandChannelIoError>,\n): Effect.Effect<A, CommandChannelIoError> =>\n\tEffect.scoped(\n\t\tEffect.gen(function* () {\n\t\t\tyield* acquireStackLock(runtimeControlLockPathForChannelFile(path)).pipe(\n\t\t\t\tEffect.mapError((cause) => new CommandChannelIoError({ path, stage, cause })),\n\t\t\t);\n\t\t\treturn yield* effect;\n\t\t}),\n\t);\n\n/** Ensure the file exists. Idempotent. Creates parent dir if needed. */\nexport const ensureFile = (path: string): Effect.Effect<void, CommandChannelIoError> =>\n\twithRuntimeControlLock(\n\t\tpath,\n\t\t'init',\n\t\tEffect.try({\n\t\t\ttry: () => {\n\t\t\t\tmkdirSync(dirname(path), { recursive: true });\n\t\t\t\tif (!existsSync(path)) {\n\t\t\t\t\twriteFileSync(path, '', { flag: 'a' });\n\t\t\t\t}\n\t\t\t},\n\t\t\tcatch: (cause) => new CommandChannelIoError({ path, stage: 'init', cause }),\n\t\t}),\n\t);\n\n/** Append one record as a single NDJSON line. The trailing newline\n * ensures the next writer's append starts a fresh record. */\nexport const appendRecord = (\n\tpath: string,\n\trecord: unknown,\n): Effect.Effect<void, CommandChannelIoError> =>\n\twithRuntimeControlLock(\n\t\tpath,\n\t\t'append',\n\t\tEffect.try({\n\t\t\ttry: () => {\n\t\t\t\tmkdirSync(dirname(path), { recursive: true });\n\t\t\t\tappendFileSync(path, `${JSON.stringify(record)}\\n`, { flag: 'a' });\n\t\t\t},\n\t\t\tcatch: (cause) => new CommandChannelIoError({ path, stage: 'append', cause }),\n\t\t}),\n\t);\n\n/** A line decoder. The channel I/O layer stays schema-agnostic so\n * callers can pick their own wire schema without this generic\n * carrying a `Schema.Decoder<unknown>` constraint. */\nexport type LineDecoder<A> = (raw: unknown) => A;\n\nconst decodeLine = <A>(\n\tpath: string,\n\tline: string,\n\tdecode: LineDecoder<A>,\n): Effect.Effect<A, CommandChannelDecodeError> =>\n\tparseJsonText(line, {\n\t\tsource: path,\n\t\tmkError: (issue) => new CommandChannelDecodeError({ path, line, cause: issue.cause ?? issue }),\n\t}).pipe(\n\t\tEffect.flatMap((parsed) =>\n\t\t\tEffect.try({\n\t\t\t\ttry: () => decode(parsed),\n\t\t\t\tcatch: (cause) => new CommandChannelDecodeError({ path, line, cause }),\n\t\t\t}),\n\t\t),\n\t);\n\n/** Read the entire current contents into structured records. Useful at\n * startup to backfill state, and as a test seam. */\nexport const readAllRecords = <A>(\n\tpath: string,\n\tdecode: LineDecoder<A>,\n): Effect.Effect<ReadonlyArray<A>, CommandChannelError> =>\n\tEffect.gen(function* () {\n\t\tif (!existsSync(path)) return [];\n\t\tconst raw = yield* Effect.try({\n\t\t\ttry: () => {\n\t\t\t\tconst fd = openSync(path, 'r');\n\t\t\t\ttry {\n\t\t\t\t\tconst { size } = statSync(path);\n\t\t\t\t\tconst buf = Buffer.alloc(size);\n\t\t\t\t\treadSync(fd, buf, 0, size, 0);\n\t\t\t\t\treturn buf.toString('utf8');\n\t\t\t\t} finally {\n\t\t\t\t\tcloseSync(fd);\n\t\t\t\t}\n\t\t\t},\n\t\t\tcatch: (cause) => new CommandChannelIoError({ path, stage: 'read', cause }),\n\t\t});\n\t\tconst lines = raw.split('\\n').filter((l) => l.length > 0);\n\t\tconst out: A[] = [];\n\t\tfor (const line of lines) {\n\t\t\tout.push(yield* decodeLine(path, line, decode));\n\t\t}\n\t\treturn out;\n\t});\n\ninterface TailState {\n\toffset: number;\n\t/** Raw, not-yet-decoded bytes of the trailing partial line. Held as a\n\t * Buffer rather than a decoded string so an incomplete UTF-8\n\t * multibyte sequence straddling a short-read boundary survives intact\n\t * until its continuation bytes arrive on the next poll. Decoding\n\t * per-read (the old behaviour) emitted U+FFFD for the split sequence\n\t * and lost the continuation bytes, which could corrupt a non-ASCII\n\t * record into invalid JSON that is then silently dropped. */\n\tpartial: Buffer;\n}\n\n/** Read newly-appended bytes from `state.offset` to current EOF. Updates\n * `state` in place. Returns the freshly-completed lines. A trailing\n * partial line (no newline yet) is preserved as raw bytes in\n * `state.partial` and prepended on the next poll. */\nconst drainNewLines = (\n\tpath: string,\n\tstate: TailState,\n): Effect.Effect<ReadonlyArray<string>, CommandChannelIoError> =>\n\tEffect.try({\n\t\ttry: () => {\n\t\t\tif (!existsSync(path)) return [];\n\t\t\tconst stat = statSync(path);\n\t\t\tif (stat.size <= state.offset) return [];\n\t\t\tconst grow = stat.size - state.offset;\n\t\t\tconst fd = openSync(path, 'r');\n\t\t\ttry {\n\t\t\t\tconst buf = Buffer.alloc(grow);\n\t\t\t\t// POSIX `read(2)` may short-return; advance the offset by\n\t\t\t\t// what was ACTUALLY read, not what we asked for. A short\n\t\t\t\t// read just means the rest will arrive on the next poll\n\t\t\t\t// iteration.\n\t\t\t\tconst bytesRead = readSync(fd, buf, 0, grow, state.offset);\n\t\t\t\tif (bytesRead <= 0) return [];\n\t\t\t\tstate.offset += bytesRead;\n\t\t\t\t// Split on the newline BYTE (0x0A) and decode only complete\n\t\t\t\t// lines. A short read can bisect a multibyte codepoint; by\n\t\t\t\t// keeping the trailing fragment as raw bytes we never decode\n\t\t\t\t// a half codepoint, so non-ASCII records reassemble byte-for-\n\t\t\t\t// byte across poll cycles. 0x0A never appears as a\n\t\t\t\t// continuation byte in UTF-8, so byte-level framing is sound.\n\t\t\t\tconst chunk = Buffer.concat([state.partial, buf.subarray(0, bytesRead)]);\n\t\t\t\tconst completedLines: string[] = [];\n\t\t\t\tlet lineStart = 0;\n\t\t\t\tfor (let i = 0; i < chunk.length; i++) {\n\t\t\t\t\tif (chunk[i] === 0x0a) {\n\t\t\t\t\t\tif (i > lineStart) {\n\t\t\t\t\t\t\tcompletedLines.push(chunk.toString('utf8', lineStart, i));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlineStart = i + 1;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// Retain the unterminated remainder (possibly an incomplete\n\t\t\t\t// multibyte sequence) as raw bytes for the next poll.\n\t\t\t\tstate.partial = Buffer.from(chunk.subarray(lineStart));\n\t\t\t\treturn completedLines;\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t},\n\t\tcatch: (cause) => new CommandChannelIoError({ path, stage: 'read', cause }),\n\t});\n\n/** Tail a file as a Stream of decoded records. The Stream is bound to\n * the surrounding Scope: when the Scope closes, polling stops.\n *\n * - `fromOffset === 'current'` skips existing content; starts at EOF.\n * - `fromOffset === 'start'` replays every existing record then tails.\n * - `fromOffset === number` resumes from a known byte offset.\n *\n * By default decode errors are surfaced as Stream failures with the\n * offending line attached so the consumer can pick a retry / skip\n * policy. Pass `onDecodeError: 'skip'` to drop the bad line instead —\n * required for NDJSON readers that race atomic-append writers (a\n * truncated/corrupt mid-flight line is normal and MUST NOT kill the\n * stream; see STYLE_GUIDE §20).\n *\n * The poll loop uses `Effect.sleep` between iterations, so cooperative\n * scheduling is preserved (no busy loop).\n */\nexport const tailRecords = <A>(\n\tpath: string,\n\tdecode: LineDecoder<A>,\n\toptions: {\n\t\treadonly fromOffset?: 'start' | 'current' | number;\n\t\treadonly pollMillis?: number;\n\t\treadonly onDecodeError?: 'fail' | 'skip';\n\t} = {},\n): Stream.Stream<A, CommandChannelError, Scope.Scope> =>\n\tStream.unwrap(\n\t\tEffect.gen(function* () {\n\t\t\tconst pollMillis = options.pollMillis ?? DEFAULT_TAIL_POLL_MILLIS;\n\t\t\tconst onDecodeError = options.onDecodeError ?? 'fail';\n\t\t\tyield* ensureFile(path);\n\t\t\tconst initialOffset = yield* Effect.try({\n\t\t\t\ttry: () => {\n\t\t\t\t\tif (options.fromOffset === 'start' || options.fromOffset === undefined) {\n\t\t\t\t\t\treturn 0;\n\t\t\t\t\t}\n\t\t\t\t\tif (options.fromOffset === 'current') {\n\t\t\t\t\t\treturn existsSync(path) ? statSync(path).size : 0;\n\t\t\t\t\t}\n\t\t\t\t\treturn options.fromOffset;\n\t\t\t\t},\n\t\t\t\tcatch: (cause) => new CommandChannelIoError({ path, stage: 'stat', cause }),\n\t\t\t});\n\t\t\tconst state: TailState = { offset: initialOffset, partial: Buffer.alloc(0) };\n\t\t\tconst pollOne: Effect.Effect<ReadonlyArray<A>, CommandChannelError> = Effect.gen(\n\t\t\t\tfunction* () {\n\t\t\t\t\tconst lines = yield* drainNewLines(path, state);\n\t\t\t\t\tif (lines.length === 0) {\n\t\t\t\t\t\tyield* Effect.sleep(`${pollMillis} millis`);\n\t\t\t\t\t\treturn [] as ReadonlyArray<A>;\n\t\t\t\t\t}\n\t\t\t\t\tconst decoded: A[] = [];\n\t\t\t\t\tfor (const line of lines) {\n\t\t\t\t\t\tif (onDecodeError === 'skip') {\n\t\t\t\t\t\t\tconst result = yield* decodeLine(path, line, decode).pipe(\n\t\t\t\t\t\t\t\tEffect.tapError((cause) =>\n\t\t\t\t\t\t\t\t\tEffect.logDebug(`tail-records: skipped malformed line at ${path}`).pipe(\n\t\t\t\t\t\t\t\t\t\tEffect.annotateLogs({ line: cause.line, cause: String(cause.cause) }),\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\tEffect.option,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tif (result._tag === 'Some') {\n\t\t\t\t\t\t\t\tdecoded.push(result.value);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tdecoded.push(yield* decodeLine(path, line, decode));\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn decoded as ReadonlyArray<A>;\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn Stream.fromEffectRepeat(pollOne).pipe(\n\t\t\t\tStream.flatMap((records) => Stream.fromIterable(records)),\n\t\t\t);\n\t\t}),\n\t);\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.mjs","names":[],"sources":["../../../../../src/substrate/runtime/cross-process/command-channel/protocol.ts"],"sourcesContent":["// Cross-process command channel — wire protocol.\n//\n// Architecture § Cross-process safety protocol § Command channel:\n// The CLI / TUI / programmable API publish typed commands to a\n// running supervisor; the supervisor publishes typed events back. The\n// channel is filesystem-backed (append-only NDJSON) so a peer\n// process can attach without IPC handshakes.\n//\n// Two files live under `<stackRoot>/`:\n// - `commands.ndjson` — caller appends; supervisor tails.\n// - `events.ndjson` — supervisor appends; callers tail.\n//\n// Each record is one JSON object per line, schema-versioned for forward\n// compatibility. Records carry a monotonically increasing sequence\n// number (per file) plus a per-record id that the publisher uses to\n// correlate event replies (e.g. `down` publishes a command, then waits\n// for an `event` carrying `correlatesTo: <same id>`).\n//\n// `EngineCommand` / `EngineEvent` payloads are NOT re-decoded on the\n// wire: the publisher API accepts typed commands, and both payload\n// kinds round-trip faithfully through JSON (closed string-tag-\n// discriminated unions with JSON-safe payloads). The channel validates\n// the envelope (`protocol`, `seq`, `id`, `correlatesTo`) and treats\n// the inner payload as `Schema.Unknown`. A future hardening pass can\n// promote the inner payloads to typed schemas without changing this\n// file's contract.\n\nimport { Schema } from 'effect';\n\n/** Protocol version. Bump on incompatible record-shape changes. */\nexport const COMMAND_CHANNEL_PROTOCOL_VERSION = 1 as const;\n\n/** Per-record envelope written to `commands.ndjson`. */\nexport const CommandRecordSchema = Schema.Struct({\n\tprotocol: Schema.Literal(COMMAND_CHANNEL_PROTOCOL_VERSION),\n\tseq: Schema.Number,\n\tid: Schema.String,\n\tat: Schema.Number,\n\tpublisherPid: Schema.Number,\n\tpublisherHostname: Schema.String,\n\tcommand: Schema.Unknown,\n});\n\nexport type CommandRecord = Schema.Schema.Type<typeof CommandRecordSchema>;\n\n/** Per-record envelope written to `events.ndjson`. The supervisor emits\n * one record per `EngineEvent` plus synthetic `ack` / `error`\n * responses when the inner handler reports completion or failure for a\n * correlatable command.\n *\n * `correlatesTo` carries the originating `CommandRecord.id` for `ack`\n * / `error`. Other event kinds (engine events) carry `null`.\n */\nexport const EventRecordSchema = Schema.Union([\n\tSchema.Struct({\n\t\tprotocol: Schema.Literal(COMMAND_CHANNEL_PROTOCOL_VERSION),\n\t\tseq: Schema.Number,\n\t\tat: Schema.Number,\n\t\tkind: Schema.Literal('engine'),\n\t\tevent: Schema.Unknown,\n\t}),\n\tSchema.Struct({\n\t\tprotocol: Schema.Literal(COMMAND_CHANNEL_PROTOCOL_VERSION),\n\t\tseq: Schema.Number,\n\t\tat: Schema.Number,\n\t\tkind: Schema.Literal('ack'),\n\t\tcorrelatesTo: Schema.String,\n\t\tdetail: Schema.optional(Schema.String),\n\t\t/** Optional structured payload (e.g. snapshot.capture's captured\n\t\t * metadata). Lets the publisher's `awaitCompletion` carry a\n\t\t * typed result through to the caller without the caller having\n\t\t * to tail the engine event stream for completion. */\n\t\tpayload: Schema.optional(Schema.Unknown),\n\t}),\n\tSchema.Struct({\n\t\tprotocol: Schema.Literal(COMMAND_CHANNEL_PROTOCOL_VERSION),\n\t\tseq: Schema.Number,\n\t\tat: Schema.Number,\n\t\tkind: Schema.Literal('error'),\n\t\tcorrelatesTo: Schema.String,\n\t\tmessage: Schema.String,\n\t\tdetail: Schema.optional(Schema.String),\n\t\t/** Optional structured payload (e.g. snapshot.captureFailed's\n\t\t * summary, snapshot.captureSkipped's reason). Mirrors the `ack`\n\t\t * payload contract — see comment above. */\n\t\tpayload: Schema.optional(Schema.Unknown),\n\t}),\n]);\n\nexport type EventRecord = Schema.Schema.Type<typeof EventRecordSchema>;\n"],"mappings":";;AAiCA,MAAa,sBAAsB,OAAO,OAAO;CAChD,UAAU,OAAO,QAAA,EAAyC;CAC1D,KAAK,OAAO;CACZ,IAAI,OAAO;CACX,IAAI,OAAO;CACX,cAAc,OAAO;CACrB,mBAAmB,OAAO;CAC1B,SAAS,OAAO;CAChB,CAAC;;;;;;;;;AAYF,MAAa,oBAAoB,OAAO,MAAM;CAC7C,OAAO,OAAO;EACb,UAAU,OAAO,QAAA,EAAyC;EAC1D,KAAK,OAAO;EACZ,IAAI,OAAO;EACX,MAAM,OAAO,QAAQ,SAAS;EAC9B,OAAO,OAAO;EACd,CAAC;CACF,OAAO,OAAO;EACb,UAAU,OAAO,QAAA,EAAyC;EAC1D,KAAK,OAAO;EACZ,IAAI,OAAO;EACX,MAAM,OAAO,QAAQ,MAAM;EAC3B,cAAc,OAAO;EACrB,QAAQ,OAAO,SAAS,OAAO,OAAO;;;;;EAKtC,SAAS,OAAO,SAAS,OAAO,QAAQ;EACxC,CAAC;CACF,OAAO,OAAO;EACb,UAAU,OAAO,QAAA,EAAyC;EAC1D,KAAK,OAAO;EACZ,IAAI,OAAO;EACX,MAAM,OAAO,QAAQ,QAAQ;EAC7B,cAAc,OAAO;EACrB,SAAS,OAAO;EAChB,QAAQ,OAAO,SAAS,OAAO,OAAO;;;;EAItC,SAAS,OAAO,SAAS,OAAO,QAAQ;EACxC,CAAC;CACF,CAAC"}
1
+ {"version":3,"file":"protocol.mjs","names":[],"sources":["../../../../../src/substrate/runtime/cross-process/command-channel/protocol.ts"],"sourcesContent":["// Cross-process command channel — wire protocol.\n//\n// Architecture § Cross-process safety protocol § Command channel:\n// The CLI / TUI / programmable API publish typed commands to a\n// running supervisor; the supervisor publishes typed events back. The\n// channel is filesystem-backed (append-only NDJSON) so a peer\n// process can attach without IPC handshakes.\n//\n// Two files live under `<stackRoot>/`:\n// - `commands.ndjson` — caller appends; supervisor tails.\n// - `events.ndjson` — supervisor appends; callers tail.\n//\n// Each record is one JSON object per line, schema-versioned for forward\n// compatibility. Records carry a monotonically increasing sequence\n// number (per file) plus a per-record id that the publisher uses to\n// correlate event replies (e.g. `down` publishes a command, then waits\n// for an `event` carrying `correlatesTo: <same id>`).\n//\n// `EngineCommand` / `EngineEvent` payloads are NOT re-decoded on the\n// wire: the publisher API accepts typed commands, and both payload\n// kinds round-trip faithfully through JSON (closed string-tag-\n// discriminated unions with JSON-safe payloads). The channel validates\n// the envelope (`protocol`, `seq`, `id`, `correlatesTo`) and treats\n// the inner payload as `Schema.Unknown`. A future hardening pass can\n// promote the inner payloads to typed schemas without changing this\n// file's contract.\n\nimport { Schema } from 'effect';\n\n/** Protocol version. Bump on incompatible record-shape changes. */\nexport const COMMAND_CHANNEL_PROTOCOL_VERSION = 1 as const;\n\n/** Per-record envelope written to `commands.ndjson`. */\nexport const CommandRecordSchema = Schema.Struct({\n\tprotocol: Schema.Literal(COMMAND_CHANNEL_PROTOCOL_VERSION),\n\tseq: Schema.Number,\n\tid: Schema.String,\n\tat: Schema.Number,\n\tpublisherPid: Schema.Number,\n\tpublisherHostname: Schema.String,\n\tcommand: Schema.Unknown,\n});\n\nexport type CommandRecord = Schema.Schema.Type<typeof CommandRecordSchema>;\n\n/** Per-record envelope written to `events.ndjson`. The supervisor emits\n * one record per `EngineEvent` plus synthetic `ack` / `error`\n * responses when the inner handler reports completion or failure for a\n * correlatable command.\n *\n * `correlatesTo` carries the originating `CommandRecord.id` for `ack`\n * / `error`. Other event kinds (engine events) carry `null`.\n */\nexport const EventRecordSchema = Schema.Union([\n\tSchema.Struct({\n\t\tprotocol: Schema.Literal(COMMAND_CHANNEL_PROTOCOL_VERSION),\n\t\tseq: Schema.Number,\n\t\tat: Schema.Number,\n\t\tkind: Schema.Literal('engine'),\n\t\tevent: Schema.Unknown,\n\t}),\n\tSchema.Struct({\n\t\tprotocol: Schema.Literal(COMMAND_CHANNEL_PROTOCOL_VERSION),\n\t\tseq: Schema.Number,\n\t\tat: Schema.Number,\n\t\tkind: Schema.Literal('ack'),\n\t\tcorrelatesTo: Schema.String,\n\t\tdetail: Schema.optional(Schema.String),\n\t\t/** Optional structured payload (e.g. snapshot.capture's captured\n\t\t * metadata). Lets the publisher's `awaitCompletion` carry a\n\t\t * typed result through to the caller without the caller having\n\t\t * to tail the engine event stream for completion. */\n\t\tpayload: Schema.optional(Schema.Unknown),\n\t}),\n\tSchema.Struct({\n\t\tprotocol: Schema.Literal(COMMAND_CHANNEL_PROTOCOL_VERSION),\n\t\tseq: Schema.Number,\n\t\tat: Schema.Number,\n\t\tkind: Schema.Literal('error'),\n\t\tcorrelatesTo: Schema.String,\n\t\tmessage: Schema.String,\n\t\tdetail: Schema.optional(Schema.String),\n\t\t/** Optional structured payload (e.g. snapshot.captureFailed's\n\t\t * summary, snapshot.captureSkipped's reason). Mirrors the `ack`\n\t\t * payload contract — see comment above. */\n\t\tpayload: Schema.optional(Schema.Unknown),\n\t}),\n]);\n\nexport type EventRecord = Schema.Schema.Type<typeof EventRecordSchema>;\n"],"mappings":";;AAiCA,MAAa,sBAAsB,OAAO,OAAO;CAChD,UAAU,OAAO,QAAA,CAAwC;CACzD,KAAK,OAAO;CACZ,IAAI,OAAO;CACX,IAAI,OAAO;CACX,cAAc,OAAO;CACrB,mBAAmB,OAAO;CAC1B,SAAS,OAAO;AACjB,CAAC;;;;;;;;;AAYD,MAAa,oBAAoB,OAAO,MAAM;CAC7C,OAAO,OAAO;EACb,UAAU,OAAO,QAAA,CAAwC;EACzD,KAAK,OAAO;EACZ,IAAI,OAAO;EACX,MAAM,OAAO,QAAQ,QAAQ;EAC7B,OAAO,OAAO;CACf,CAAC;CACD,OAAO,OAAO;EACb,UAAU,OAAO,QAAA,CAAwC;EACzD,KAAK,OAAO;EACZ,IAAI,OAAO;EACX,MAAM,OAAO,QAAQ,KAAK;EAC1B,cAAc,OAAO;EACrB,QAAQ,OAAO,SAAS,OAAO,MAAM;;;;;EAKrC,SAAS,OAAO,SAAS,OAAO,OAAO;CACxC,CAAC;CACD,OAAO,OAAO;EACb,UAAU,OAAO,QAAA,CAAwC;EACzD,KAAK,OAAO;EACZ,IAAI,OAAO;EACX,MAAM,OAAO,QAAQ,OAAO;EAC5B,cAAc,OAAO;EACrB,SAAS,OAAO;EAChB,QAAQ,OAAO,SAAS,OAAO,MAAM;;;;EAIrC,SAAS,OAAO,SAAS,OAAO,OAAO;CACxC,CAAC;AACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-control-lock.mjs","names":[],"sources":["../../../../../src/substrate/runtime/cross-process/command-channel/runtime-control-lock.ts"],"sourcesContent":["import { dirname } from 'node:path';\n\nexport const RUNTIME_CONTROL_LOCK_FILE_SUFFIX = '.runtime-control.lock';\n\nexport const runtimeControlLockPathForStackRoot = (stackRoot: string): string =>\n\t`${stackRoot}${RUNTIME_CONTROL_LOCK_FILE_SUFFIX}`;\n\nexport const runtimeControlLockPathForChannelFile = (path: string): string =>\n\truntimeControlLockPathForStackRoot(dirname(path));\n"],"mappings":";;AAEA,MAAa,mCAAmC;AAEhD,MAAa,sCAAsC,cAClD,GAAG,YAAY;AAEhB,MAAa,wCAAwC,SACpD,mCAAmC,QAAQ,KAAK,CAAC"}
1
+ {"version":3,"file":"runtime-control-lock.mjs","names":[],"sources":["../../../../../src/substrate/runtime/cross-process/command-channel/runtime-control-lock.ts"],"sourcesContent":["import { dirname } from 'node:path';\n\nexport const RUNTIME_CONTROL_LOCK_FILE_SUFFIX = '.runtime-control.lock';\n\nexport const runtimeControlLockPathForStackRoot = (stackRoot: string): string =>\n\t`${stackRoot}${RUNTIME_CONTROL_LOCK_FILE_SUFFIX}`;\n\nexport const runtimeControlLockPathForChannelFile = (path: string): string =>\n\truntimeControlLockPathForStackRoot(dirname(path));\n"],"mappings":";;AAEA,MAAa,mCAAmC;AAEhD,MAAa,sCAAsC,cAClD,GAAG,YAAY;AAEhB,MAAa,wCAAwC,SACpD,mCAAmC,QAAQ,IAAI,CAAC"}
@@ -6,6 +6,4 @@ import "./command-channel/file-channel.mjs";
6
6
  import "./command-channel/channel.mjs";
7
7
  import "./command-channel/index.mjs";
8
8
  import "./roster.mjs";
9
- import "./snapshot-reservation.mjs";
10
- import "./lock.mjs";
11
9
  export {};
@@ -26,7 +26,7 @@ const LIVE_CLOCK = {
26
26
  * lock retry backoff, holder-liveness PID probes). Callers MUST NOT
27
27
  * wrap user-supplied body effects — those should inherit the caller's
28
28
  * clock so TestClock-driven tests can adjust virtual time inside
29
- * critical sections. See `cross-process/lock.ts` for the discipline. */
29
+ * critical sections. See `cross-process/stack-lock.ts` for the discipline. */
30
30
  const underLiveClock = (effect) => Effect.provideService(effect, Clock.Clock, LIVE_CLOCK);
31
31
  //#endregion
32
32
  export { underLiveClock };
@@ -1 +1 @@
1
- {"version":3,"file":"live-clock.mjs","names":[],"sources":["../../../../src/substrate/runtime/cross-process/live-clock.ts"],"sourcesContent":["// Live wall-clock instance — shared between the cross-process lock and\n// the on-disk `stack.lock` acquire loop.\n//\n// Cross-process safety is fundamentally a wall-time property: two OS\n// processes can't share a virtual test clock, and the holder-liveness\n// PID/start-time probes that reclaim stale locks measure real time. The\n// `underLiveClock` wrapper pins acquire/release infrastructure (lock\n// retry backoff, holder-liveness probes) to this clock so the lock\n// primitive's semantics hold even when callers install a `TestClock`.\n//\n// SCOPE: applied ONLY to acquire/release infrastructure. The user body\n// inherits the caller's clock so TestClock-driven tests can virtualize\n// `Effect.sleep` inside lock-protected sections without being forced to\n// wall time. The lock primitives themselves still get the live clock\n// for their wall-time invariants. The SHORT-CRITICAL-SECTION discipline\n// from the architecture is enforced by code review rather than by\n// forcing wall time on every body.\n\nimport { Clock, Duration, Effect } from 'effect';\n\n/** Live system Clock instance. Used by the cross-process lock and the\n * on-disk `stack.lock` acquire loop so the lock's wall-time invariants\n * (acquire-timeout, exponential backoff, holder-liveness probes) hold\n * even when the surrounding fiber has a `TestClock` installed. */\nexport const LIVE_CLOCK: Clock.Clock = {\n\tcurrentTimeMillis: Effect.sync(() => Date.now()),\n\tcurrentTimeMillisUnsafe: () => Date.now(),\n\tcurrentTimeNanos: Effect.sync(() => BigInt(Date.now()) * 1_000_000n),\n\tcurrentTimeNanosUnsafe: () => BigInt(Date.now()) * 1_000_000n,\n\tsleep: (duration: Duration.Duration) =>\n\t\tEffect.callback<void>((resume) => {\n\t\t\tconst ms = Duration.toMillis(duration);\n\t\t\tif (ms <= 0) {\n\t\t\t\tresume(Effect.void);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst handle = setTimeout(() => resume(Effect.void), ms);\n\t\t\treturn Effect.sync(() => clearTimeout(handle));\n\t\t}),\n};\n\n/** Provide the live Clock for `effect`, overriding any inherited\n * `TestClock` for the duration of the wrapped effect.\n *\n * SCOPE: applied ONLY to the acquire/release infrastructure (OS-advisory\n * lock retry backoff, holder-liveness PID probes). Callers MUST NOT\n * wrap user-supplied body effects — those should inherit the caller's\n * clock so TestClock-driven tests can adjust virtual time inside\n * critical sections. See `cross-process/lock.ts` for the discipline. */\nexport const underLiveClock = <A, E, R>(\n\teffect: Effect.Effect<A, E, R>,\n): Effect.Effect<A, E, R> => Effect.provideService(effect, Clock.Clock, LIVE_CLOCK);\n"],"mappings":";;;;;;AAwBA,MAAa,aAA0B;CACtC,mBAAmB,OAAO,WAAW,KAAK,KAAK,CAAC;CAChD,+BAA+B,KAAK,KAAK;CACzC,kBAAkB,OAAO,WAAW,OAAO,KAAK,KAAK,CAAC,GAAG,SAAW;CACpE,8BAA8B,OAAO,KAAK,KAAK,CAAC,GAAG;CACnD,QAAQ,aACP,OAAO,UAAgB,WAAW;EACjC,MAAM,KAAK,SAAS,SAAS,SAAS;AACtC,MAAI,MAAM,GAAG;AACZ,UAAO,OAAO,KAAK;AACnB;;EAED,MAAM,SAAS,iBAAiB,OAAO,OAAO,KAAK,EAAE,GAAG;AACxD,SAAO,OAAO,WAAW,aAAa,OAAO,CAAC;GAC7C;CACH;;;;;;;;;AAUD,MAAa,kBACZ,WAC4B,OAAO,eAAe,QAAQ,MAAM,OAAO,WAAW"}
1
+ {"version":3,"file":"live-clock.mjs","names":[],"sources":["../../../../src/substrate/runtime/cross-process/live-clock.ts"],"sourcesContent":["// Live wall-clock instance — shared between the cross-process lock and\n// the on-disk `stack.lock` acquire loop.\n//\n// Cross-process safety is fundamentally a wall-time property: two OS\n// processes can't share a virtual test clock, and the holder-liveness\n// PID/start-time probes that reclaim stale locks measure real time. The\n// `underLiveClock` wrapper pins acquire/release infrastructure (lock\n// retry backoff, holder-liveness probes) to this clock so the lock\n// primitive's semantics hold even when callers install a `TestClock`.\n//\n// SCOPE: applied ONLY to acquire/release infrastructure. The user body\n// inherits the caller's clock so TestClock-driven tests can virtualize\n// `Effect.sleep` inside lock-protected sections without being forced to\n// wall time. The lock primitives themselves still get the live clock\n// for their wall-time invariants. The SHORT-CRITICAL-SECTION discipline\n// from the architecture is enforced by code review rather than by\n// forcing wall time on every body.\n\nimport { Clock, Duration, Effect } from 'effect';\n\n/** Live system Clock instance. Used by the cross-process lock and the\n * on-disk `stack.lock` acquire loop so the lock's wall-time invariants\n * (acquire-timeout, exponential backoff, holder-liveness probes) hold\n * even when the surrounding fiber has a `TestClock` installed. */\nexport const LIVE_CLOCK: Clock.Clock = {\n\tcurrentTimeMillis: Effect.sync(() => Date.now()),\n\tcurrentTimeMillisUnsafe: () => Date.now(),\n\tcurrentTimeNanos: Effect.sync(() => BigInt(Date.now()) * 1_000_000n),\n\tcurrentTimeNanosUnsafe: () => BigInt(Date.now()) * 1_000_000n,\n\tsleep: (duration: Duration.Duration) =>\n\t\tEffect.callback<void>((resume) => {\n\t\t\tconst ms = Duration.toMillis(duration);\n\t\t\tif (ms <= 0) {\n\t\t\t\tresume(Effect.void);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst handle = setTimeout(() => resume(Effect.void), ms);\n\t\t\treturn Effect.sync(() => clearTimeout(handle));\n\t\t}),\n};\n\n/** Provide the live Clock for `effect`, overriding any inherited\n * `TestClock` for the duration of the wrapped effect.\n *\n * SCOPE: applied ONLY to the acquire/release infrastructure (OS-advisory\n * lock retry backoff, holder-liveness PID probes). Callers MUST NOT\n * wrap user-supplied body effects — those should inherit the caller's\n * clock so TestClock-driven tests can adjust virtual time inside\n * critical sections. See `cross-process/stack-lock.ts` for the discipline. */\nexport const underLiveClock = <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>\n\tEffect.provideService(effect, Clock.Clock, LIVE_CLOCK);\n"],"mappings":";;;;;;AAwBA,MAAa,aAA0B;CACtC,mBAAmB,OAAO,WAAW,KAAK,IAAI,CAAC;CAC/C,+BAA+B,KAAK,IAAI;CACxC,kBAAkB,OAAO,WAAW,OAAO,KAAK,IAAI,CAAC,IAAI,QAAU;CACnE,8BAA8B,OAAO,KAAK,IAAI,CAAC,IAAI;CACnD,QAAQ,aACP,OAAO,UAAgB,WAAW;EACjC,MAAM,KAAK,SAAS,SAAS,QAAQ;EACrC,IAAI,MAAM,GAAG;GACZ,OAAO,OAAO,IAAI;GAClB;EACD;EACA,MAAM,SAAS,iBAAiB,OAAO,OAAO,IAAI,GAAG,EAAE;EACvD,OAAO,OAAO,WAAW,aAAa,MAAM,CAAC;CAC9C,CAAC;AACH;;;;;;;;;AAUA,MAAa,kBAA2B,WACvC,OAAO,eAAe,QAAQ,MAAM,OAAO,UAAU"}
@@ -99,21 +99,14 @@ const hashStartTimeStamp = (stamp) => {
99
99
  return h >>> 0;
100
100
  };
101
101
  /** Liveness probe for a roster holder. Used by the claim-protocol
102
- * sweep AND the snapshot-reservation orphan check.
102
+ * sweep AND the stack-lock one-shot orphan check.
103
103
  *
104
- * Discipline:
105
- * - Foreign-host (`hostname` differs from our own) → ALWAYS alive
106
- * (NFS-safe default; cross-host pid comparisons are meaningless).
107
- * - Same-host: pid must be live AND start-time must match.
104
+ * Discipline (devstack is single-host): pid must be live AND start-time
105
+ * must match the recorded stamp.
108
106
  *
109
107
  * Returns `'alive' | 'dead'`. Never throws. Effect-wrapped so callers
110
108
  * compose under spans. */
111
- const checkHolderLiveness = Effect.fn("cross-process.liveness.checkHolderLiveness")(function* (holder, ownHost = hostname(), cache) {
112
- if (holder.hostname !== ownHost) return "alive";
113
- yield* Effect.annotateCurrentSpan({
114
- "devstack.holder.pid": holder.pid,
115
- "devstack.holder.host": holder.hostname
116
- });
109
+ const checkHolderLiveness = Effect.fn("cross-process.liveness.checkHolderLiveness")(function* (holder, cache) {
117
110
  if (!isPidAlive(holder.pid)) return "dead";
118
111
  const probedStart = processStartTime(holder.pid, cache);
119
112
  if (probedStart === null) return "alive";
@@ -121,15 +114,15 @@ const checkHolderLiveness = Effect.fn("cross-process.liveness.checkHolderLivenes
121
114
  return probedStart === holder.startTime ? "alive" : "dead";
122
115
  });
123
116
  /** Build a holder snapshot for THIS process. The intent defaults to
124
- * `'normal'`; the snapshot-reservation flow flips it to `'snapshot'`
125
- * under the stack lock and back when the reservation releases.
117
+ * `'normal'`; the snapshot bounce flips it to `'snapshot'`
118
+ * under the stack lock and back when the bounce completes.
126
119
  *
127
120
  * A `null` `startTime` propagates verbatim — readers (`isOwnEntry`
128
121
  * in `roster.ts`, `checkHolderLiveness` above) honor the null-
129
- * conservative branch. Writing `0` for "unprobable" was the prior
130
- * shape and caused a false-dead harvest: a subsequent probe yielding
131
- * a real stamp would mismatch the recorded `0` and the process could
132
- * no longer recognize its own entry. */
122
+ * conservative branch. Writing `0` for "unprobable" would cause a
123
+ * false-dead harvest: a subsequent probe yielding a real stamp would
124
+ * mismatch the recorded `0` and the process would fail to recognize
125
+ * its own entry. */
133
126
  const ownHolder = (intent = "normal") => {
134
127
  const pid = selfPid();
135
128
  return {
@@ -152,7 +145,7 @@ const layerLivenessProbeScope = Layer.effect(LivenessProbeScope, Effect.sync(()
152
145
  const cache = /* @__PURE__ */ new Map();
153
146
  return LivenessProbeScope.of({
154
147
  probeStartTime: (pid) => processStartTime(pid, cache),
155
- probeHolderLiveness: (holder, ownHost = hostname()) => checkHolderLiveness(holder, ownHost, cache)
148
+ probeHolderLiveness: (holder) => checkHolderLiveness(holder, cache)
156
149
  });
157
150
  }));
158
151
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"liveness.mjs","names":["nodeHostname"],"sources":["../../../../src/substrate/runtime/cross-process/liveness.ts"],"sourcesContent":["// PID + start-time liveness check.\n//\n// Architecture § Cross-process safety protocol § Claim protocol step 3:\n// \"process exists AND its start-time (as read from `/proc/<pid>/stat` on\n// Linux, `ps -o lstart` on macOS, or equivalent) matches the recorded\n// `startTime`.\"\n//\n// Foreign-host entries are treated as alive (NFS-safe conservative\n// default). On the same host, two checks combine: `kill(pid, 0)`\n// determines pid-in-use; `ps -o lstart` confirms it's the SAME process\n// (defending against pid reuse on long-uptime machines).\n//\n// This module is the ONLY place in the substrate that calls into\n// `process.kill` and shells out for start times. Roster/snapshot\n// reservation/stack-lock all consult it through the typed predicates.\n\nimport { execFileSync } from 'node:child_process';\nimport { hostname as nodeHostname } from 'node:os';\n\nimport { Context, Effect, Layer } from 'effect';\n\nimport type { RosterHolder } from '../../cross-process.ts';\nimport { selfPid } from './self-pid.ts';\n\n/** Cheap \"send signal 0\" pid liveness. ESRCH → dead; EPERM → alive\n * (foreign-user pid on shared dev hosts). Any other errno is\n * conservatively treated as dead so we don't refuse cleanup on exotic\n * platforms. */\nexport const isPidAlive = (pid: number): boolean => {\n\tif (!Number.isFinite(pid) || pid <= 0) return false;\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (err) {\n\t\tconst code = (err as NodeJS.ErrnoException).code;\n\t\treturn code === 'EPERM';\n\t}\n};\n\n/** Per-sweep cache mapping `pid → start-time stamp` (or `null` when the\n * pid is gone/unprobable). Callers that issue many probes in a single\n * sweep (roster step-3 stale-eviction, container-claim ledger prune,\n * etc.) instantiate one cache per pass and thread it through every\n * probe call so the same pid forks `ps`/`tasklist` AT MOST once per\n * sweep. Single-shot callers omit the cache and get the no-cache\n * behavior.\n *\n * Discriminator is `Map.has(pid)` — a cached `null` is a real result\n * (the pid is missing) and skipping it would re-fork pointlessly. */\nexport type LivenessCache = Map<number, number | null>;\n\n/** Best-effort start-time stamp for `pid`. Returns `null` when the\n * process is gone OR the platform can't produce a time. The stamp\n * itself is opaque text — its only contract is bytewise equality with\n * a previously-recorded sibling stamp.\n *\n * Architecture § Cross-process safety protocol §3 — `ps -o lstart` on\n * macOS/Linux, `tasklist` confirmation on Windows.\n *\n * Pass `cache` to reuse a previously-probed result across a sweep\n * pass — same `pid` only forks the underlying utility once. */\nexport const processStartTime = (pid: number, cache?: LivenessCache): number | null => {\n\tif (!Number.isFinite(pid) || pid <= 0) return null;\n\t// Our own pid's startTime never changes during the process\n\t// lifetime — cache the first SUCCESSFUL probe forever. Eliminates\n\t// the `ps` spawn under high in-process contention (e.g. N fibers\n\t// fighting over a single cross-process lock all probing each\n\t// other's shared pid). Without this cache, 8 concurrent fibers\n\t// each fork `ps -o lstart` with a 2s timeout, compounding into\n\t// seconds of latency that exhaust the claim budget (review fix\n\t// phase 22f reclaim-stress reproducer caught it).\n\t//\n\t// Only a non-null result is memoized: a `null` here means the\n\t// probe transiently FAILED (spawn hiccup / 2s timeout under load),\n\t// not that the pid is gone — it's our own pid, which is alive by\n\t// definition. Caching that null forever would surrender this\n\t// process's PID-reuse protection (it would write `startTime: null`\n\t// to disk for its whole lifetime) on a single flaky fork. Leaving\n\t// the cache UNSET re-probes on the next call; an occasional extra\n\t// `ps` on a previously-failing probe is negligible versus losing\n\t// reuse protection for the lifetime.\n\tif (pid === selfPid()) {\n\t\tif (ownStartTimeCache !== UNSET) return ownStartTimeCache;\n\t\tconst probed = probeStartTimeUncached(pid);\n\t\tif (probed !== null) ownStartTimeCache = probed;\n\t\treturn probed;\n\t}\n\tif (cache?.has(pid)) {\n\t\t// Map.get is `T | undefined` — but `has` is true, so the value\n\t\t// is one of the cached results (a `number` or `null`).\n\t\treturn cache.get(pid) ?? null;\n\t}\n\tconst probed = probeStartTimeUncached(pid);\n\tcache?.set(pid, probed);\n\treturn probed;\n};\n\nconst UNSET: unique symbol = Symbol('UNSET');\n// Only a successful (non-null) self-pid probe is ever stored here; a\n// failed probe leaves this UNSET so the next call re-probes.\nlet ownStartTimeCache: number | typeof UNSET = UNSET;\n\n/** Inner probe — always forks the platform utility. Split out so the\n * cache branch in `processStartTime` stays a single read/write. */\nconst probeStartTimeUncached = (pid: number): number | null => {\n\tif (process.platform === 'win32') {\n\t\ttry {\n\t\t\tconst out = execFileSync('tasklist', ['/fi', `PID eq ${pid}`, '/fo', 'csv', '/nh'], {\n\t\t\t\tencoding: 'utf8',\n\t\t\t\ttimeout: 2000,\n\t\t\t\tstdio: ['ignore', 'pipe', 'ignore'],\n\t\t\t});\n\t\t\t// Windows path: just hash the line. Windows PID reuse on uptime\n\t\t\t// is a known v1 trade-off; the architecture says the protocol\n\t\t\t// stays POSIX-first.\n\t\t\treturn out.trim().startsWith('\"') ? hashStartTimeStamp(out.trim()) : null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\ttry {\n\t\tconst out = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {\n\t\t\tencoding: 'utf8',\n\t\t\ttimeout: 2000,\n\t\t\tstdio: ['ignore', 'pipe', 'ignore'],\n\t\t});\n\t\tconst trimmed = out.trim();\n\t\treturn trimmed.length > 0 ? hashStartTimeStamp(trimmed) : null;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\n/** Stable hash of a start-time string. Reduces the textual `ps -o\n * lstart=` output to a single number so the roster carries a fixed-\n * width integer (the architecture's `startTime` field is `number`).\n *\n * FNV-1a 32-bit; collisions are negligible at the (pid, host, second)\n * granularity we ever compare. */\nconst hashStartTimeStamp = (stamp: string): number => {\n\tlet h = 2166136261;\n\tfor (let i = 0; i < stamp.length; i++) {\n\t\th ^= stamp.charCodeAt(i);\n\t\th = (h * 16777619) >>> 0;\n\t}\n\t// Stay inside Number.MAX_SAFE_INTEGER's positive 32-bit range so the\n\t// number round-trips through JSON without precision loss.\n\treturn h >>> 0;\n};\n\n/** Liveness probe for a roster holder. Used by the claim-protocol\n * sweep AND the snapshot-reservation orphan check.\n *\n * Discipline:\n * - Foreign-host (`hostname` differs from our own) → ALWAYS alive\n * (NFS-safe default; cross-host pid comparisons are meaningless).\n * - Same-host: pid must be live AND start-time must match.\n *\n * Returns `'alive' | 'dead'`. Never throws. Effect-wrapped so callers\n * compose under spans. */\nexport const checkHolderLiveness = Effect.fn('cross-process.liveness.checkHolderLiveness')(\n\tfunction* (holder: RosterHolder, ownHost: string = nodeHostname(), cache?: LivenessCache) {\n\t\t// Foreign-host: NFS-safe — we can't verify, assume alive.\n\t\tif (holder.hostname !== ownHost) {\n\t\t\treturn 'alive' as const;\n\t\t}\n\t\tyield* Effect.annotateCurrentSpan({\n\t\t\t'devstack.holder.pid': holder.pid,\n\t\t\t'devstack.holder.host': holder.hostname,\n\t\t});\n\t\tif (!isPidAlive(holder.pid)) return 'dead' as const;\n\t\tconst probedStart = processStartTime(holder.pid, cache);\n\t\t// pid alive but no stamp probable → conservative: ALIVE\n\t\t// (we have nothing to dispute the recorded startTime with).\n\t\tif (probedStart === null) return 'alive' as const;\n\t\t// Holder recorded a `null` startTime (writer's platform couldn't\n\t\t// probe at the time). The (pid, hostname) pair carries the\n\t\t// identity; same conservative policy as the probedStart-null\n\t\t// branch. Mismatching a real probed stamp against a recorded\n\t\t// `null` would otherwise harvest live holders as \"dead\".\n\t\tif (holder.startTime === null) return 'alive' as const;\n\t\treturn probedStart === holder.startTime ? ('alive' as const) : ('dead' as const);\n\t},\n);\n\n/** Build a holder snapshot for THIS process. The intent defaults to\n * `'normal'`; the snapshot-reservation flow flips it to `'snapshot'`\n * under the stack lock and back when the reservation releases.\n *\n * A `null` `startTime` propagates verbatim — readers (`isOwnEntry`\n * in `roster.ts`, `checkHolderLiveness` above) honor the null-\n * conservative branch. Writing `0` for \"unprobable\" was the prior\n * shape and caused a false-dead harvest: a subsequent probe yielding\n * a real stamp would mismatch the recorded `0` and the process could\n * no longer recognize its own entry. */\nexport const ownHolder = (intent: 'normal' | 'snapshot' = 'normal'): RosterHolder => {\n\tconst pid = selfPid();\n\tconst startTime = processStartTime(pid);\n\treturn {\n\t\tpid,\n\t\tstartTime,\n\t\thostname: nodeHostname(),\n\t\tclaimedAt: Date.now(),\n\t\theartbeatAt: Date.now(),\n\t\tintent,\n\t};\n};\n\n// -----------------------------------------------------------------------------\n// LivenessProbeScope — per-sweep cache, lifted out of the Map parameter.\n// -----------------------------------------------------------------------------\n//\n// The bare `LivenessCache = Map<pid, stamp | null>` was threaded through\n// `roster.sweepStaleHolders` and `liveContainerClaims` as an explicit\n// parameter, which is fine for two sites but doesn't scale to the three\n// other sweep loops (dispatch routes, doctor probes, lifecycle-prune)\n// that haven't migrated yet. Promoting the cache into a service lets a\n// sweep loop `yield* LivenessProbeScope` once and call the probe\n// methods without re-passing the Map. The optional-Map params on\n// `processStartTime` / `checkHolderLiveness` stay so unmigrated callers\n// can keep working from outside an Effect.\n\n/** Methods a per-sweep scope exposes — the captured cache backs both\n * so the same `pid` forks `ps`/`tasklist` at most once per scope. */\nexport interface LivenessProbeScopeShape {\n\treadonly probeStartTime: (pid: number) => number | null;\n\treadonly probeHolderLiveness: (\n\t\tholder: RosterHolder,\n\t\townHost?: string,\n\t) => Effect.Effect<'alive' | 'dead'>;\n}\n\n/** A per-sweep liveness scope. Callers `yield* LivenessProbeScope`\n * once per sweep (typically inside an `Effect.scoped` block that\n * wraps the loop) and call `probeStartTime` / `probeHolderLiveness`\n * on each holder without threading the Map manually. */\nexport class LivenessProbeScope extends Context.Service<\n\tLivenessProbeScope,\n\tLivenessProbeScopeShape\n>()('@devstack/substrate/cross-process/LivenessProbeScope') {}\n\n/** Construct a fresh `LivenessProbeScope` whose underlying cache is\n * private to this layer — every yielding scope gets its own Map. */\nexport const layerLivenessProbeScope: Layer.Layer<LivenessProbeScope> = Layer.effect(\n\tLivenessProbeScope,\n\tEffect.sync(() => {\n\t\tconst cache: LivenessCache = new Map();\n\t\treturn LivenessProbeScope.of({\n\t\t\tprobeStartTime: (pid) => processStartTime(pid, cache),\n\t\t\tprobeHolderLiveness: (holder, ownHost = nodeHostname()) =>\n\t\t\t\tcheckHolderLiveness(holder, ownHost, cache),\n\t\t});\n\t}),\n);\n"],"mappings":";;;;;;;;;AA4BA,MAAa,cAAc,QAAyB;AACnD,KAAI,CAAC,OAAO,SAAS,IAAI,IAAI,OAAO,EAAG,QAAO;AAC9C,KAAI;AACH,UAAQ,KAAK,KAAK,EAAE;AACpB,SAAO;UACC,KAAK;AAEb,SADc,IAA8B,SAC5B;;;;;;;;;;;;;AA0BlB,MAAa,oBAAoB,KAAa,UAAyC;AACtF,KAAI,CAAC,OAAO,SAAS,IAAI,IAAI,OAAO,EAAG,QAAO;AAmB9C,KAAI,QAAQ,SAAS,EAAE;AACtB,MAAI,sBAAsB,MAAO,QAAO;EACxC,MAAM,SAAS,uBAAuB,IAAI;AAC1C,MAAI,WAAW,KAAM,qBAAoB;AACzC,SAAO;;AAER,KAAI,OAAO,IAAI,IAAI,CAGlB,QAAO,MAAM,IAAI,IAAI,IAAI;CAE1B,MAAM,SAAS,uBAAuB,IAAI;AAC1C,QAAO,IAAI,KAAK,OAAO;AACvB,QAAO;;AAGR,MAAM,QAAuB,OAAO,QAAQ;AAG5C,IAAI,oBAA2C;;;AAI/C,MAAM,0BAA0B,QAA+B;AAC9D,KAAI,QAAQ,aAAa,QACxB,KAAI;EACH,MAAM,MAAM,aAAa,YAAY;GAAC;GAAO,UAAU;GAAO;GAAO;GAAO;GAAM,EAAE;GACnF,UAAU;GACV,SAAS;GACT,OAAO;IAAC;IAAU;IAAQ;IAAS;GACnC,CAAC;AAIF,SAAO,IAAI,MAAM,CAAC,WAAW,KAAI,GAAG,mBAAmB,IAAI,MAAM,CAAC,GAAG;SAC9D;AACP,SAAO;;AAGT,KAAI;EAMH,MAAM,UALM,aAAa,MAAM;GAAC;GAAM;GAAW;GAAM,OAAO,IAAI;GAAC,EAAE;GACpE,UAAU;GACV,SAAS;GACT,OAAO;IAAC;IAAU;IAAQ;IAAS;GACnC,CACkB,CAAC,MAAM;AAC1B,SAAO,QAAQ,SAAS,IAAI,mBAAmB,QAAQ,GAAG;SACnD;AACP,SAAO;;;;;;;;;AAUT,MAAM,sBAAsB,UAA0B;CACrD,IAAI,IAAI;AACR,MAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACtC,OAAK,MAAM,WAAW,EAAE;AACxB,MAAK,IAAI,aAAc;;AAIxB,QAAO,MAAM;;;;;;;;;;;;AAad,MAAa,sBAAsB,OAAO,GAAG,6CAA6C,CACzF,WAAW,QAAsB,UAAkBA,UAAc,EAAE,OAAuB;AAEzF,KAAI,OAAO,aAAa,QACvB,QAAO;AAER,QAAO,OAAO,oBAAoB;EACjC,uBAAuB,OAAO;EAC9B,wBAAwB,OAAO;EAC/B,CAAC;AACF,KAAI,CAAC,WAAW,OAAO,IAAI,CAAE,QAAO;CACpC,MAAM,cAAc,iBAAiB,OAAO,KAAK,MAAM;AAGvD,KAAI,gBAAgB,KAAM,QAAO;AAMjC,KAAI,OAAO,cAAc,KAAM,QAAO;AACtC,QAAO,gBAAgB,OAAO,YAAa,UAAqB;EAEjE;;;;;;;;;;;AAYD,MAAa,aAAa,SAAgC,aAA2B;CACpF,MAAM,MAAM,SAAS;AAErB,QAAO;EACN;EACA,WAHiB,iBAAiB,IAGzB;EACT,UAAUA,UAAc;EACxB,WAAW,KAAK,KAAK;EACrB,aAAa,KAAK,KAAK;EACvB;EACA;;;;;;AA+BF,IAAa,qBAAb,cAAwC,QAAQ,SAG7C,CAAC,uDAAuD,CAAC;;;AAI5D,MAAa,0BAA2D,MAAM,OAC7E,oBACA,OAAO,WAAW;CACjB,MAAM,wBAAuB,IAAI,KAAK;AACtC,QAAO,mBAAmB,GAAG;EAC5B,iBAAiB,QAAQ,iBAAiB,KAAK,MAAM;EACrD,sBAAsB,QAAQ,UAAUA,UAAc,KACrD,oBAAoB,QAAQ,SAAS,MAAM;EAC5C,CAAC;EACD,CACF"}
1
+ {"version":3,"file":"liveness.mjs","names":["nodeHostname"],"sources":["../../../../src/substrate/runtime/cross-process/liveness.ts"],"sourcesContent":["// PID + start-time liveness check.\n//\n// Architecture § Cross-process safety protocol § Claim protocol step 3:\n// \"process exists AND its start-time (as read from `/proc/<pid>/stat` on\n// Linux, `ps -o lstart` on macOS, or equivalent) matches the recorded\n// `startTime`.\"\n//\n// Devstack is single-host: two checks combine. `kill(pid, 0)`\n// determines pid-in-use; `ps -o lstart` confirms it's the SAME process\n// (defending against pid reuse on long-uptime machines).\n//\n// This module is the ONLY place in the substrate that calls into\n// `process.kill` and shells out for start times. Roster/snapshot\n// reservation/stack-lock all consult it through the typed predicates.\n\nimport { execFileSync } from 'node:child_process';\nimport { hostname as nodeHostname } from 'node:os';\n\nimport { Context, Effect, Layer } from 'effect';\n\nimport type { RosterHolder } from '../../cross-process.ts';\nimport { selfPid } from './self-pid.ts';\n\n/** Cheap \"send signal 0\" pid liveness. ESRCH → dead; EPERM → alive\n * (foreign-user pid on shared dev hosts). Any other errno is\n * conservatively treated as dead so we don't refuse cleanup on exotic\n * platforms. */\nexport const isPidAlive = (pid: number): boolean => {\n\tif (!Number.isFinite(pid) || pid <= 0) return false;\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (err) {\n\t\tconst code = (err as NodeJS.ErrnoException).code;\n\t\treturn code === 'EPERM';\n\t}\n};\n\n/** Per-sweep cache mapping `pid → start-time stamp` (or `null` when the\n * pid is gone/unprobable). Callers that issue many probes in a single\n * sweep (roster step-3 stale-eviction, container-claim ledger prune,\n * etc.) instantiate one cache per pass and thread it through every\n * probe call so the same pid forks `ps`/`tasklist` AT MOST once per\n * sweep. Single-shot callers omit the cache and get the no-cache\n * behavior.\n *\n * Discriminator is `Map.has(pid)` — a cached `null` is a real result\n * (the pid is missing) and skipping it would re-fork pointlessly. */\nexport type LivenessCache = Map<number, number | null>;\n\n/** Best-effort start-time stamp for `pid`. Returns `null` when the\n * process is gone OR the platform can't produce a time. The stamp\n * itself is opaque text — its only contract is bytewise equality with\n * a previously-recorded sibling stamp.\n *\n * Architecture § Cross-process safety protocol §3 — `ps -o lstart` on\n * macOS/Linux, `tasklist` confirmation on Windows.\n *\n * Pass `cache` to reuse a previously-probed result across a sweep\n * pass — same `pid` only forks the underlying utility once. */\nexport const processStartTime = (pid: number, cache?: LivenessCache): number | null => {\n\tif (!Number.isFinite(pid) || pid <= 0) return null;\n\t// Our own pid's startTime never changes during the process\n\t// lifetime — cache the first SUCCESSFUL probe forever. Eliminates\n\t// the `ps` spawn under high in-process contention (e.g. N fibers\n\t// fighting over a single cross-process lock all probing each\n\t// other's shared pid). Without this cache, 8 concurrent fibers\n\t// each fork `ps -o lstart` with a 2s timeout, compounding into\n\t// seconds of latency that exhaust the claim budget (review fix\n\t// phase 22f reclaim-stress reproducer caught it).\n\t//\n\t// Only a non-null result is memoized: a `null` here means the\n\t// probe transiently FAILED (spawn hiccup / 2s timeout under load),\n\t// not that the pid is gone — it's our own pid, which is alive by\n\t// definition. Caching that null forever would surrender this\n\t// process's PID-reuse protection (it would write `startTime: null`\n\t// to disk for its whole lifetime) on a single flaky fork. Leaving\n\t// the cache UNSET re-probes on the next call; an occasional extra\n\t// `ps` on a previously-failing probe is negligible versus losing\n\t// reuse protection for the lifetime.\n\tif (pid === selfPid()) {\n\t\tif (ownStartTimeCache !== UNSET) return ownStartTimeCache;\n\t\tconst probed = probeStartTimeUncached(pid);\n\t\tif (probed !== null) ownStartTimeCache = probed;\n\t\treturn probed;\n\t}\n\tif (cache?.has(pid)) {\n\t\t// Map.get is `T | undefined` — but `has` is true, so the value\n\t\t// is one of the cached results (a `number` or `null`).\n\t\treturn cache.get(pid) ?? null;\n\t}\n\tconst probed = probeStartTimeUncached(pid);\n\tcache?.set(pid, probed);\n\treturn probed;\n};\n\nconst UNSET: unique symbol = Symbol('UNSET');\n// Only a successful (non-null) self-pid probe is ever stored here; a\n// failed probe leaves this UNSET so the next call re-probes.\nlet ownStartTimeCache: number | typeof UNSET = UNSET;\n\n/** Inner probe — always forks the platform utility. Split out so the\n * cache branch in `processStartTime` stays a single read/write. */\nconst probeStartTimeUncached = (pid: number): number | null => {\n\tif (process.platform === 'win32') {\n\t\ttry {\n\t\t\tconst out = execFileSync('tasklist', ['/fi', `PID eq ${pid}`, '/fo', 'csv', '/nh'], {\n\t\t\t\tencoding: 'utf8',\n\t\t\t\ttimeout: 2000,\n\t\t\t\tstdio: ['ignore', 'pipe', 'ignore'],\n\t\t\t});\n\t\t\t// Windows path: just hash the line. Windows PID reuse on uptime\n\t\t\t// is a known v1 trade-off; the architecture says the protocol\n\t\t\t// stays POSIX-first.\n\t\t\treturn out.trim().startsWith('\"') ? hashStartTimeStamp(out.trim()) : null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\ttry {\n\t\tconst out = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {\n\t\t\tencoding: 'utf8',\n\t\t\ttimeout: 2000,\n\t\t\tstdio: ['ignore', 'pipe', 'ignore'],\n\t\t});\n\t\tconst trimmed = out.trim();\n\t\treturn trimmed.length > 0 ? hashStartTimeStamp(trimmed) : null;\n\t} catch {\n\t\treturn null;\n\t}\n};\n\n/** Stable hash of a start-time string. Reduces the textual `ps -o\n * lstart=` output to a single number so the roster carries a fixed-\n * width integer (the architecture's `startTime` field is `number`).\n *\n * FNV-1a 32-bit; collisions are negligible at the (pid, host, second)\n * granularity we ever compare. */\nconst hashStartTimeStamp = (stamp: string): number => {\n\tlet h = 2166136261;\n\tfor (let i = 0; i < stamp.length; i++) {\n\t\th ^= stamp.charCodeAt(i);\n\t\th = (h * 16777619) >>> 0;\n\t}\n\t// Stay inside Number.MAX_SAFE_INTEGER's positive 32-bit range so the\n\t// number round-trips through JSON without precision loss.\n\treturn h >>> 0;\n};\n\n/** Liveness probe for a roster holder. Used by the claim-protocol\n * sweep AND the stack-lock one-shot orphan check.\n *\n * Discipline (devstack is single-host): pid must be live AND start-time\n * must match the recorded stamp.\n *\n * Returns `'alive' | 'dead'`. Never throws. Effect-wrapped so callers\n * compose under spans. */\nexport const checkHolderLiveness = Effect.fn('cross-process.liveness.checkHolderLiveness')(\n\tfunction* (holder: RosterHolder, cache?: LivenessCache) {\n\t\tif (!isPidAlive(holder.pid)) return 'dead' as const;\n\t\tconst probedStart = processStartTime(holder.pid, cache);\n\t\t// pid alive but no stamp probable → conservative: ALIVE\n\t\t// (we have nothing to dispute the recorded startTime with).\n\t\tif (probedStart === null) return 'alive' as const;\n\t\t// Holder recorded a `null` startTime (writer's platform couldn't\n\t\t// probe at the time). The (pid, hostname) pair carries the\n\t\t// identity; same conservative policy as the probedStart-null\n\t\t// branch. Mismatching a real probed stamp against a recorded\n\t\t// `null` would otherwise harvest live holders as \"dead\".\n\t\tif (holder.startTime === null) return 'alive' as const;\n\t\treturn probedStart === holder.startTime ? ('alive' as const) : ('dead' as const);\n\t},\n);\n\n/** Build a holder snapshot for THIS process. The intent defaults to\n * `'normal'`; the snapshot bounce flips it to `'snapshot'`\n * under the stack lock and back when the bounce completes.\n *\n * A `null` `startTime` propagates verbatim — readers (`isOwnEntry`\n * in `roster.ts`, `checkHolderLiveness` above) honor the null-\n * conservative branch. Writing `0` for \"unprobable\" would cause a\n * false-dead harvest: a subsequent probe yielding a real stamp would\n * mismatch the recorded `0` and the process would fail to recognize\n * its own entry. */\nexport const ownHolder = (intent: 'normal' | 'snapshot' = 'normal'): RosterHolder => {\n\tconst pid = selfPid();\n\tconst startTime = processStartTime(pid);\n\treturn {\n\t\tpid,\n\t\tstartTime,\n\t\thostname: nodeHostname(),\n\t\tclaimedAt: Date.now(),\n\t\theartbeatAt: Date.now(),\n\t\tintent,\n\t};\n};\n\n// -----------------------------------------------------------------------------\n// LivenessProbeScope — per-sweep cache, lifted out of the Map parameter.\n// -----------------------------------------------------------------------------\n//\n// The bare `LivenessCache = Map<pid, stamp | null>` was threaded through\n// `roster.sweepStaleHolders` as an explicit parameter, which is fine for\n// one site but doesn't scale to the other sweep loops (dispatch routes,\n// doctor probes, lifecycle-prune). Promoting the cache into a service\n// lets a sweep loop `yield* LivenessProbeScope` once and call the probe\n// methods without re-passing the Map. The optional-Map params on\n// `processStartTime` / `checkHolderLiveness` stay so unmigrated callers\n// can keep working from outside an Effect.\n\n/** Methods a per-sweep scope exposes — the captured cache backs both\n * so the same `pid` forks `ps`/`tasklist` at most once per scope. */\nexport interface LivenessProbeScopeShape {\n\treadonly probeStartTime: (pid: number) => number | null;\n\treadonly probeHolderLiveness: (holder: RosterHolder) => Effect.Effect<'alive' | 'dead'>;\n}\n\n/** A per-sweep liveness scope. Callers `yield* LivenessProbeScope`\n * once per sweep (typically inside an `Effect.scoped` block that\n * wraps the loop) and call `probeStartTime` / `probeHolderLiveness`\n * on each holder without threading the Map manually. */\nexport class LivenessProbeScope extends Context.Service<\n\tLivenessProbeScope,\n\tLivenessProbeScopeShape\n>()('@devstack/substrate/cross-process/LivenessProbeScope') {}\n\n/** Construct a fresh `LivenessProbeScope` whose underlying cache is\n * private to this layer — every yielding scope gets its own Map. */\nexport const layerLivenessProbeScope: Layer.Layer<LivenessProbeScope> = Layer.effect(\n\tLivenessProbeScope,\n\tEffect.sync(() => {\n\t\tconst cache: LivenessCache = new Map();\n\t\treturn LivenessProbeScope.of({\n\t\t\tprobeStartTime: (pid) => processStartTime(pid, cache),\n\t\t\tprobeHolderLiveness: (holder) => checkHolderLiveness(holder, cache),\n\t\t});\n\t}),\n);\n"],"mappings":";;;;;;;;;AA2BA,MAAa,cAAc,QAAyB;CACnD,IAAI,CAAC,OAAO,SAAS,GAAG,KAAK,OAAO,GAAG,OAAO;CAC9C,IAAI;EACH,QAAQ,KAAK,KAAK,CAAC;EACnB,OAAO;CACR,SAAS,KAAK;EAEb,OADc,IAA8B,SAC5B;CACjB;AACD;;;;;;;;;;;AAwBA,MAAa,oBAAoB,KAAa,UAAyC;CACtF,IAAI,CAAC,OAAO,SAAS,GAAG,KAAK,OAAO,GAAG,OAAO;CAmB9C,IAAI,QAAQ,QAAQ,GAAG;EACtB,IAAI,sBAAsB,OAAO,OAAO;EACxC,MAAM,SAAS,uBAAuB,GAAG;EACzC,IAAI,WAAW,MAAM,oBAAoB;EACzC,OAAO;CACR;CACA,IAAI,OAAO,IAAI,GAAG,GAGjB,OAAO,MAAM,IAAI,GAAG,KAAK;CAE1B,MAAM,SAAS,uBAAuB,GAAG;CACzC,OAAO,IAAI,KAAK,MAAM;CACtB,OAAO;AACR;AAEA,MAAM,QAAuB,OAAO,OAAO;AAG3C,IAAI,oBAA2C;;;AAI/C,MAAM,0BAA0B,QAA+B;CAC9D,IAAI,QAAQ,aAAa,SACxB,IAAI;EACH,MAAM,MAAM,aAAa,YAAY;GAAC;GAAO,UAAU;GAAO;GAAO;GAAO;EAAK,GAAG;GACnF,UAAU;GACV,SAAS;GACT,OAAO;IAAC;IAAU;IAAQ;GAAQ;EACnC,CAAC;EAID,OAAO,IAAI,KAAK,CAAC,CAAC,WAAW,IAAG,IAAI,mBAAmB,IAAI,KAAK,CAAC,IAAI;CACtE,QAAQ;EACP,OAAO;CACR;CAED,IAAI;EAMH,MAAM,UALM,aAAa,MAAM;GAAC;GAAM;GAAW;GAAM,OAAO,GAAG;EAAC,GAAG;GACpE,UAAU;GACV,SAAS;GACT,OAAO;IAAC;IAAU;IAAQ;GAAQ;EACnC,CACkB,CAAC,CAAC,KAAK;EACzB,OAAO,QAAQ,SAAS,IAAI,mBAAmB,OAAO,IAAI;CAC3D,QAAQ;EACP,OAAO;CACR;AACD;;;;;;;AAQA,MAAM,sBAAsB,UAA0B;CACrD,IAAI,IAAI;CACR,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACtC,KAAK,MAAM,WAAW,CAAC;EACvB,IAAK,IAAI,aAAc;CACxB;CAGA,OAAO,MAAM;AACd;;;;;;;;;AAUA,MAAa,sBAAsB,OAAO,GAAG,4CAA4C,CAAC,CACzF,WAAW,QAAsB,OAAuB;CACvD,IAAI,CAAC,WAAW,OAAO,GAAG,GAAG,OAAO;CACpC,MAAM,cAAc,iBAAiB,OAAO,KAAK,KAAK;CAGtD,IAAI,gBAAgB,MAAM,OAAO;CAMjC,IAAI,OAAO,cAAc,MAAM,OAAO;CACtC,OAAO,gBAAgB,OAAO,YAAa,UAAqB;AACjE,CACD;;;;;;;;;;;AAYA,MAAa,aAAa,SAAgC,aAA2B;CACpF,MAAM,MAAM,QAAQ;CAEpB,OAAO;EACN;EACA,WAHiB,iBAAiB,GAG1B;EACR,UAAUA,SAAa;EACvB,WAAW,KAAK,IAAI;EACpB,aAAa,KAAK,IAAI;EACtB;CACD;AACD;;;;;AA0BA,IAAa,qBAAb,cAAwC,QAAQ,QAG9C,CAAC,CAAC,sDAAsD,CAAC,CAAC,CAAC;;;AAI7D,MAAa,0BAA2D,MAAM,OAC7E,oBACA,OAAO,WAAW;CACjB,MAAM,wBAAuB,IAAI,IAAI;CACrC,OAAO,mBAAmB,GAAG;EAC5B,iBAAiB,QAAQ,iBAAiB,KAAK,KAAK;EACpD,sBAAsB,WAAW,oBAAoB,QAAQ,KAAK;CACnE,CAAC;AACF,CAAC,CACF"}
@@ -29,8 +29,8 @@ const readOrNull = (path) => {
29
29
  *
30
30
  * `parse` is the caller's body decoder; it returns a non-null value for
31
31
  * a well-formed body and `null` for an unparseable one. We never look
32
- * at the decoded value — only at whether it parsed — so the lock and
33
- * the reservation can share one guard despite carrying different
32
+ * at the decoded value — only at whether it parsed — so different
33
+ * coordination files can share one guard despite carrying different
34
34
  * schemas.
35
35
  *
36
36
  * The staleness threshold is `DEFAULT_SWEEP_POLICY.staleAfterMillis`
@@ -1 +1 @@
1
- {"version":3,"file":"reclaim-stale-file.mjs","names":[],"sources":["../../../../src/substrate/runtime/cross-process/reclaim-stale-file.ts"],"sourcesContent":["// Shared reclaim guard for unparseable/orphaned coordination files.\n//\n// Both `stack.lock` (stack-lock.ts) and `snapshot.reservation`\n// (snapshot-reservation.ts) reclaim a file whose on-disk body fails to\n// parse (a peer that crashed mid-write) once it ages past the roster\n// sweep's staleness window. The PID liveness check has nothing to\n// consult when the body won't parse, so mtime + the shared staleness\n// budget is the only abandonment signal.\n//\n// The naive shape — `statSync` for the mtime, then `unlinkSync` — has a\n// TOCTOU window: between reading the mtime and unlinking, a competing\n// process can legitimately reclaim the garbage lock and write a FRESH,\n// VALID O_EXCL body. The unconditional `unlinkSync` would then clobber\n// that live lock, producing two simultaneous holders (a mutual-\n// exclusion break). This module NARROWS the window — it does not fully\n// eliminate it — by RE-STATTING the file immediately before the unlink\n// and confirming it is STILL the same stale inode (same mtime + ino +\n// dev) AND still unparseable. If anything changed, the reclaim bails\n// and the caller falls back to its normal backoff/retry path so the\n// competitor's fresh body survives.\n//\n// Residual race (deliberately accepted): the final `unlinkSync` is\n// path-based and UNCONDITIONAL, so between the re-stat/re-read and the\n// syscall a competing reclaimer can still unlink+O_EXCL-create a fresh\n// body that this unlink then deletes. Closing it fully would need an\n// atomic, inode-pinned delete (`renameat2(RENAME_EXCHANGE)` or an fd\n// held across the check) that `node:fs` does not portably expose — a\n// plain `rename(path, …)` is itself path-based and would relocate the\n// competitor's fresh body just as readily, so it buys nothing. The\n// re-stat shrinks the window from \"mtime read → unlink\" (a parse +\n// staleness comparison wide) to \"re-read → unlink\" (a couple of\n// syscalls), making a real collision require both racers mid-reclaim of\n// the SAME file with sub-microsecond timing. Given that, the residual\n// is left open rather than papered over with an equally-racy rename.\n//\n// The dead-PID liveness reclaim path is NOT routed through here: that\n// branch is gated on a holder-liveness probe (see liveness.ts) and is\n// already safe — a dead holder cannot have written a fresh body. Only\n// the unparseable/mtime-stale branch carries the TOCTOU, so only it\n// needs this guard.\n//\n// Sync `node:fs` by design — the call sites are inside `Effect.try`\n// critical sections that need a non-blocking attempt; the rest of the\n// substrate stays Effect-native.\n\nimport { readFileSync, statSync, unlinkSync } from 'node:fs';\n\nimport { DEFAULT_SWEEP_POLICY } from '../../cross-process.ts';\n\n/** Outcome of a reclaim attempt.\n *\n * - `reclaimed` — the file was an aged, unparseable orphan and we\n * unlinked it (or it vanished from under us mid-reclaim, which is the\n * same observable end state: the slot is free for the next O_EXCL).\n * - `parseable` — the body parsed, so it is NOT an orphan; a live peer\n * holds it (possibly one that just reclaimed the garbage ahead of\n * us). Do not touch it.\n * - `fresh` — the body is unparseable but its mtime is inside the\n * staleness window; a writer is presumed mid-flush. Respect the\n * budget and leave it.\n * - `changed` — the re-stat guard fired: the file's identity (mtime /\n * ino / dev) or parse-state changed between the staleness check and\n * the unlink, meaning a competitor reclaimed the slot and wrote a\n * fresh body in the window. We bailed to avoid clobbering it.\n * - `absent` — the file was already gone when we looked. */\nexport type ReclaimOutcome = 'reclaimed' | 'parseable' | 'fresh' | 'changed' | 'absent';\n\n/** A minimal, comparable identity for a file on disk. Two reads of the\n * same untouched inode produce equal triples; any rewrite (O_EXCL\n * unlink+create, or an in-place touch) perturbs at least one field. */\ninterface FileIdentity {\n\treadonly mtimeMs: number;\n\treadonly ino: number;\n\treadonly dev: number;\n}\n\nconst identityOf = (path: string): FileIdentity | null => {\n\ttry {\n\t\tconst s = statSync(path);\n\t\treturn { mtimeMs: s.mtimeMs, ino: s.ino, dev: s.dev };\n\t} catch {\n\t\t// ENOENT (gone) or any transient stat failure — treat as absent.\n\t\treturn null;\n\t}\n};\n\nconst sameIdentity = (a: FileIdentity, b: FileIdentity): boolean =>\n\ta.mtimeMs === b.mtimeMs && a.ino === b.ino && a.dev === b.dev;\n\nconst readOrNull = (path: string): string | null => {\n\ttry {\n\t\treturn readFileSync(path, 'utf8');\n\t} catch {\n\t\treturn null;\n\t}\n};\n\n/** Reclaim `path` IFF it is an aged, unparseable orphan — with a re-stat\n * guard that NARROWS (not closes) the TOCTOU window between the\n * staleness check and the unlink. See the module header for the\n * deliberately-accepted sub-microsecond residual between the final\n * re-read and the path-based unlink.\n *\n * `parse` is the caller's body decoder; it returns a non-null value for\n * a well-formed body and `null` for an unparseable one. We never look\n * at the decoded value — only at whether it parsed — so the lock and\n * the reservation can share one guard despite carrying different\n * schemas.\n *\n * The staleness threshold is `DEFAULT_SWEEP_POLICY.staleAfterMillis`\n * (30s), matching the roster's eviction window: anything that crashed\n * mid-write at least 30s ago is presumed abandoned.\n *\n * Never throws. Any I/O failure resolves to a conservative non-\n * reclaiming outcome so a transient stat/read hiccup never triggers a\n * spurious unlink — the caller's backoff loop retries on the next pass. */\nexport const reclaimUnparseableStaleFile = (\n\tpath: string,\n\tparse: (raw: string) => unknown,\n\tstaleAfterMillis: number = DEFAULT_SWEEP_POLICY.staleAfterMillis,\n): ReclaimOutcome => {\n\t// 1. Initial identity + staleness.\n\tconst before = identityOf(path);\n\tif (before === null) return 'absent';\n\n\tconst rawBefore = readOrNull(path);\n\tif (rawBefore === null) return 'absent';\n\tif (parse(rawBefore) !== null) return 'parseable';\n\n\tconst isStale = Date.now() - before.mtimeMs > staleAfterMillis;\n\tif (!isStale) return 'fresh';\n\n\t// 2. Re-stat guard — NARROWS (does not close) the TOCTOU. Between the\n\t// read above and the unlink below, a competitor may have reclaimed\n\t// the garbage and written a FRESH valid O_EXCL body. Re-confirm the\n\t// file is STILL the same stale inode AND still unparseable before\n\t// unlinking. A sub-microsecond residual remains between this\n\t// re-read and the path-based unlink (see header) — accepted.\n\tconst after = identityOf(path);\n\tif (after === null) {\n\t\t// Vanished from under us — a sibling reclaimer won the unlink. The\n\t\t// slot is free; report it as reclaimed (same observable result).\n\t\treturn 'reclaimed';\n\t}\n\tif (!sameIdentity(before, after)) return 'changed';\n\n\tconst rawAfter = readOrNull(path);\n\tif (rawAfter === null) return 'reclaimed';\n\tif (parse(rawAfter) !== null) return 'changed';\n\n\t// 3. Still the same stale, unparseable orphan — safe to unlink.\n\ttry {\n\t\tunlinkSync(path);\n\t} catch {\n\t\t// Race with another reclaimer that unlinked first — harmless; the\n\t\t// slot is free either way.\n\t}\n\treturn 'reclaimed';\n};\n"],"mappings":";;;AA4EA,MAAM,cAAc,SAAsC;AACzD,KAAI;EACH,MAAM,IAAI,SAAS,KAAK;AACxB,SAAO;GAAE,SAAS,EAAE;GAAS,KAAK,EAAE;GAAK,KAAK,EAAE;GAAK;SAC9C;AAEP,SAAO;;;AAIT,MAAM,gBAAgB,GAAiB,MACtC,EAAE,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE;AAE3D,MAAM,cAAc,SAAgC;AACnD,KAAI;AACH,SAAO,aAAa,MAAM,OAAO;SAC1B;AACP,SAAO;;;;;;;;;;;;;;;;;;;;;;AAuBT,MAAa,+BACZ,MACA,OACA,mBAA2B,qBAAqB,qBAC5B;CAEpB,MAAM,SAAS,WAAW,KAAK;AAC/B,KAAI,WAAW,KAAM,QAAO;CAE5B,MAAM,YAAY,WAAW,KAAK;AAClC,KAAI,cAAc,KAAM,QAAO;AAC/B,KAAI,MAAM,UAAU,KAAK,KAAM,QAAO;AAGtC,KAAI,EADY,KAAK,KAAK,GAAG,OAAO,UAAU,kBAChC,QAAO;CAQrB,MAAM,QAAQ,WAAW,KAAK;AAC9B,KAAI,UAAU,KAGb,QAAO;AAER,KAAI,CAAC,aAAa,QAAQ,MAAM,CAAE,QAAO;CAEzC,MAAM,WAAW,WAAW,KAAK;AACjC,KAAI,aAAa,KAAM,QAAO;AAC9B,KAAI,MAAM,SAAS,KAAK,KAAM,QAAO;AAGrC,KAAI;AACH,aAAW,KAAK;SACT;AAIR,QAAO"}
1
+ {"version":3,"file":"reclaim-stale-file.mjs","names":[],"sources":["../../../../src/substrate/runtime/cross-process/reclaim-stale-file.ts"],"sourcesContent":["// Shared reclaim guard for unparseable/orphaned coordination files.\n//\n// `stack.lock` (stack-lock.ts) reclaims a file whose on-disk body fails\n// to parse (a peer that crashed mid-write) once it ages past the roster\n// sweep's staleness window. The PID liveness check has nothing to\n// consult when the body won't parse, so mtime + the shared staleness\n// budget is the only abandonment signal. The guard is generic over the\n// body schema so any future coordination file can reuse it.\n//\n// The naive shape — `statSync` for the mtime, then `unlinkSync` — has a\n// TOCTOU window: between reading the mtime and unlinking, a competing\n// process can legitimately reclaim the garbage lock and write a FRESH,\n// VALID O_EXCL body. The unconditional `unlinkSync` would then clobber\n// that live lock, producing two simultaneous holders (a mutual-\n// exclusion break). This module NARROWS the window — it does not fully\n// eliminate it — by RE-STATTING the file immediately before the unlink\n// and confirming it is STILL the same stale inode (same mtime + ino +\n// dev) AND still unparseable. If anything changed, the reclaim bails\n// and the caller falls back to its normal backoff/retry path so the\n// competitor's fresh body survives.\n//\n// Residual race (deliberately accepted): the final `unlinkSync` is\n// path-based and UNCONDITIONAL, so between the re-stat/re-read and the\n// syscall a competing reclaimer can still unlink+O_EXCL-create a fresh\n// body that this unlink then deletes. Closing it fully would need an\n// atomic, inode-pinned delete (`renameat2(RENAME_EXCHANGE)` or an fd\n// held across the check) that `node:fs` does not portably expose — a\n// plain `rename(path, …)` is itself path-based and would relocate the\n// competitor's fresh body just as readily, so it buys nothing. The\n// re-stat shrinks the window from \"mtime read → unlink\" (a parse +\n// staleness comparison wide) to \"re-read → unlink\" (a couple of\n// syscalls), making a real collision require both racers mid-reclaim of\n// the SAME file with sub-microsecond timing. Given that, the residual\n// is left open rather than papered over with an equally-racy rename.\n//\n// The dead-PID liveness reclaim path is NOT routed through here: that\n// branch is gated on a holder-liveness probe (see liveness.ts) and is\n// already safe — a dead holder cannot have written a fresh body. Only\n// the unparseable/mtime-stale branch carries the TOCTOU, so only it\n// needs this guard.\n//\n// Sync `node:fs` by design — the call sites are inside `Effect.try`\n// critical sections that need a non-blocking attempt; the rest of the\n// substrate stays Effect-native.\n\nimport { readFileSync, statSync, unlinkSync } from 'node:fs';\n\nimport { DEFAULT_SWEEP_POLICY } from '../../cross-process.ts';\n\n/** Outcome of a reclaim attempt.\n *\n * - `reclaimed` — the file was an aged, unparseable orphan and we\n * unlinked it (or it vanished from under us mid-reclaim, which is the\n * same observable end state: the slot is free for the next O_EXCL).\n * - `parseable` — the body parsed, so it is NOT an orphan; a live peer\n * holds it (possibly one that just reclaimed the garbage ahead of\n * us). Do not touch it.\n * - `fresh` — the body is unparseable but its mtime is inside the\n * staleness window; a writer is presumed mid-flush. Respect the\n * budget and leave it.\n * - `changed` — the re-stat guard fired: the file's identity (mtime /\n * ino / dev) or parse-state changed between the staleness check and\n * the unlink, meaning a competitor reclaimed the slot and wrote a\n * fresh body in the window. We bailed to avoid clobbering it.\n * - `absent` — the file was already gone when we looked. */\nexport type ReclaimOutcome = 'reclaimed' | 'parseable' | 'fresh' | 'changed' | 'absent';\n\n/** A minimal, comparable identity for a file on disk. Two reads of the\n * same untouched inode produce equal triples; any rewrite (O_EXCL\n * unlink+create, or an in-place touch) perturbs at least one field. */\ninterface FileIdentity {\n\treadonly mtimeMs: number;\n\treadonly ino: number;\n\treadonly dev: number;\n}\n\nconst identityOf = (path: string): FileIdentity | null => {\n\ttry {\n\t\tconst s = statSync(path);\n\t\treturn { mtimeMs: s.mtimeMs, ino: s.ino, dev: s.dev };\n\t} catch {\n\t\t// ENOENT (gone) or any transient stat failure — treat as absent.\n\t\treturn null;\n\t}\n};\n\nconst sameIdentity = (a: FileIdentity, b: FileIdentity): boolean =>\n\ta.mtimeMs === b.mtimeMs && a.ino === b.ino && a.dev === b.dev;\n\nconst readOrNull = (path: string): string | null => {\n\ttry {\n\t\treturn readFileSync(path, 'utf8');\n\t} catch {\n\t\treturn null;\n\t}\n};\n\n/** Reclaim `path` IFF it is an aged, unparseable orphan — with a re-stat\n * guard that NARROWS (not closes) the TOCTOU window between the\n * staleness check and the unlink. See the module header for the\n * deliberately-accepted sub-microsecond residual between the final\n * re-read and the path-based unlink.\n *\n * `parse` is the caller's body decoder; it returns a non-null value for\n * a well-formed body and `null` for an unparseable one. We never look\n * at the decoded value — only at whether it parsed — so different\n * coordination files can share one guard despite carrying different\n * schemas.\n *\n * The staleness threshold is `DEFAULT_SWEEP_POLICY.staleAfterMillis`\n * (30s), matching the roster's eviction window: anything that crashed\n * mid-write at least 30s ago is presumed abandoned.\n *\n * Never throws. Any I/O failure resolves to a conservative non-\n * reclaiming outcome so a transient stat/read hiccup never triggers a\n * spurious unlink — the caller's backoff loop retries on the next pass. */\nexport const reclaimUnparseableStaleFile = (\n\tpath: string,\n\tparse: (raw: string) => unknown,\n\tstaleAfterMillis: number = DEFAULT_SWEEP_POLICY.staleAfterMillis,\n): ReclaimOutcome => {\n\t// 1. Initial identity + staleness.\n\tconst before = identityOf(path);\n\tif (before === null) return 'absent';\n\n\tconst rawBefore = readOrNull(path);\n\tif (rawBefore === null) return 'absent';\n\tif (parse(rawBefore) !== null) return 'parseable';\n\n\tconst isStale = Date.now() - before.mtimeMs > staleAfterMillis;\n\tif (!isStale) return 'fresh';\n\n\t// 2. Re-stat guard — NARROWS (does not close) the TOCTOU. Between the\n\t// read above and the unlink below, a competitor may have reclaimed\n\t// the garbage and written a FRESH valid O_EXCL body. Re-confirm the\n\t// file is STILL the same stale inode AND still unparseable before\n\t// unlinking. A sub-microsecond residual remains between this\n\t// re-read and the path-based unlink (see header) — accepted.\n\tconst after = identityOf(path);\n\tif (after === null) {\n\t\t// Vanished from under us — a sibling reclaimer won the unlink. The\n\t\t// slot is free; report it as reclaimed (same observable result).\n\t\treturn 'reclaimed';\n\t}\n\tif (!sameIdentity(before, after)) return 'changed';\n\n\tconst rawAfter = readOrNull(path);\n\tif (rawAfter === null) return 'reclaimed';\n\tif (parse(rawAfter) !== null) return 'changed';\n\n\t// 3. Still the same stale, unparseable orphan — safe to unlink.\n\ttry {\n\t\tunlinkSync(path);\n\t} catch {\n\t\t// Race with another reclaimer that unlinked first — harmless; the\n\t\t// slot is free either way.\n\t}\n\treturn 'reclaimed';\n};\n"],"mappings":";;;AA4EA,MAAM,cAAc,SAAsC;CACzD,IAAI;EACH,MAAM,IAAI,SAAS,IAAI;EACvB,OAAO;GAAE,SAAS,EAAE;GAAS,KAAK,EAAE;GAAK,KAAK,EAAE;EAAI;CACrD,QAAQ;EAEP,OAAO;CACR;AACD;AAEA,MAAM,gBAAgB,GAAiB,MACtC,EAAE,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE;AAE3D,MAAM,cAAc,SAAgC;CACnD,IAAI;EACH,OAAO,aAAa,MAAM,MAAM;CACjC,QAAQ;EACP,OAAO;CACR;AACD;;;;;;;;;;;;;;;;;;;;AAqBA,MAAa,+BACZ,MACA,OACA,mBAA2B,qBAAqB,qBAC5B;CAEpB,MAAM,SAAS,WAAW,IAAI;CAC9B,IAAI,WAAW,MAAM,OAAO;CAE5B,MAAM,YAAY,WAAW,IAAI;CACjC,IAAI,cAAc,MAAM,OAAO;CAC/B,IAAI,MAAM,SAAS,MAAM,MAAM,OAAO;CAGtC,IAAI,EADY,KAAK,IAAI,IAAI,OAAO,UAAU,mBAChC,OAAO;CAQrB,MAAM,QAAQ,WAAW,IAAI;CAC7B,IAAI,UAAU,MAGb,OAAO;CAER,IAAI,CAAC,aAAa,QAAQ,KAAK,GAAG,OAAO;CAEzC,MAAM,WAAW,WAAW,IAAI;CAChC,IAAI,aAAa,MAAM,OAAO;CAC9B,IAAI,MAAM,QAAQ,MAAM,MAAM,OAAO;CAGrC,IAAI;EACH,WAAW,IAAI;CAChB,QAAQ,CAGR;CACA,OAAO;AACR"}