@omnitron-dev/omnitron 0.2.0 → 0.3.1

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 (1299) hide show
  1. package/README.md +4 -0
  2. package/dist/BUILD.json +5 -0
  3. package/dist/cli/omnitron.js +375 -29
  4. package/dist/cli/omnitron.js.map +1 -1
  5. package/dist/cluster/cluster.rpc-service.d.ts +40 -0
  6. package/dist/cluster/cluster.rpc-service.d.ts.map +1 -1
  7. package/dist/cluster/cluster.rpc-service.js +49 -3
  8. package/dist/cluster/cluster.rpc-service.js.map +1 -1
  9. package/dist/cluster/config-sync.d.ts.map +1 -1
  10. package/dist/cluster/config-sync.js.map +1 -1
  11. package/dist/cluster/leader-election.d.ts +90 -2
  12. package/dist/cluster/leader-election.d.ts.map +1 -1
  13. package/dist/cluster/leader-election.js +176 -22
  14. package/dist/cluster/leader-election.js.map +1 -1
  15. package/dist/cluster/mesh-link.d.ts +136 -0
  16. package/dist/cluster/mesh-link.d.ts.map +1 -0
  17. package/dist/cluster/mesh-link.js +212 -0
  18. package/dist/cluster/mesh-link.js.map +1 -0
  19. package/dist/cluster/mesh.d.ts +59 -0
  20. package/dist/cluster/mesh.d.ts.map +1 -0
  21. package/dist/cluster/mesh.js +117 -0
  22. package/dist/cluster/mesh.js.map +1 -0
  23. package/dist/cluster/slave-connector.d.ts +193 -9
  24. package/dist/cluster/slave-connector.d.ts.map +1 -1
  25. package/dist/cluster/slave-connector.js +409 -37
  26. package/dist/cluster/slave-connector.js.map +1 -1
  27. package/dist/commands/audit.d.ts +65 -0
  28. package/dist/commands/audit.d.ts.map +1 -0
  29. package/dist/commands/audit.js +233 -0
  30. package/dist/commands/audit.js.map +1 -0
  31. package/dist/commands/backup.d.ts +4 -0
  32. package/dist/commands/backup.d.ts.map +1 -1
  33. package/dist/commands/backup.js +255 -33
  34. package/dist/commands/backup.js.map +1 -1
  35. package/dist/commands/cluster.d.ts.map +1 -1
  36. package/dist/commands/cluster.js +78 -47
  37. package/dist/commands/cluster.js.map +1 -1
  38. package/dist/commands/daemon-cmd.d.ts +7 -1
  39. package/dist/commands/daemon-cmd.d.ts.map +1 -1
  40. package/dist/commands/daemon-cmd.js +87 -21
  41. package/dist/commands/daemon-cmd.js.map +1 -1
  42. package/dist/commands/daemon-required.d.ts +34 -0
  43. package/dist/commands/daemon-required.d.ts.map +1 -0
  44. package/dist/commands/daemon-required.js +92 -0
  45. package/dist/commands/daemon-required.js.map +1 -0
  46. package/dist/commands/deploy.d.ts +70 -8
  47. package/dist/commands/deploy.d.ts.map +1 -1
  48. package/dist/commands/deploy.js +99 -64
  49. package/dist/commands/deploy.js.map +1 -1
  50. package/dist/commands/discover.d.ts +25 -4
  51. package/dist/commands/discover.d.ts.map +1 -1
  52. package/dist/commands/discover.js +155 -102
  53. package/dist/commands/discover.js.map +1 -1
  54. package/dist/commands/doctor.d.ts +293 -0
  55. package/dist/commands/doctor.d.ts.map +1 -0
  56. package/dist/commands/doctor.js +1706 -0
  57. package/dist/commands/doctor.js.map +1 -0
  58. package/dist/commands/env.d.ts +8 -1
  59. package/dist/commands/env.d.ts.map +1 -1
  60. package/dist/commands/env.js +13 -8
  61. package/dist/commands/env.js.map +1 -1
  62. package/dist/commands/exec.d.ts.map +1 -1
  63. package/dist/commands/exec.js +3 -2
  64. package/dist/commands/exec.js.map +1 -1
  65. package/dist/commands/fleet-asking.d.ts +87 -0
  66. package/dist/commands/fleet-asking.d.ts.map +1 -0
  67. package/dist/commands/fleet-asking.js +176 -0
  68. package/dist/commands/fleet-asking.js.map +1 -0
  69. package/dist/commands/fleet-recover.d.ts +2 -0
  70. package/dist/commands/fleet-recover.d.ts.map +1 -0
  71. package/dist/commands/fleet-recover.js +30 -0
  72. package/dist/commands/fleet-recover.js.map +1 -0
  73. package/dist/commands/fleet.d.ts +18 -0
  74. package/dist/commands/fleet.d.ts.map +1 -1
  75. package/dist/commands/fleet.js +314 -74
  76. package/dist/commands/fleet.js.map +1 -1
  77. package/dist/commands/health-check.d.ts +8 -6
  78. package/dist/commands/health-check.d.ts.map +1 -1
  79. package/dist/commands/health-check.js +26 -23
  80. package/dist/commands/health-check.js.map +1 -1
  81. package/dist/commands/health.d.ts.map +1 -1
  82. package/dist/commands/health.js +22 -8
  83. package/dist/commands/health.js.map +1 -1
  84. package/dist/commands/host-ingress.d.ts +3 -0
  85. package/dist/commands/host-ingress.d.ts.map +1 -0
  86. package/dist/commands/host-ingress.js +31 -0
  87. package/dist/commands/host-ingress.js.map +1 -0
  88. package/dist/commands/infra.d.ts +42 -1
  89. package/dist/commands/infra.d.ts.map +1 -1
  90. package/dist/commands/infra.js +418 -49
  91. package/dist/commands/infra.js.map +1 -1
  92. package/dist/commands/init.d.ts.map +1 -1
  93. package/dist/commands/init.js +21 -36
  94. package/dist/commands/init.js.map +1 -1
  95. package/dist/commands/inspect-graph.d.ts.map +1 -1
  96. package/dist/commands/inspect-graph.js +2 -6
  97. package/dist/commands/inspect-graph.js.map +1 -1
  98. package/dist/commands/inspect.d.ts.map +1 -1
  99. package/dist/commands/inspect.js +110 -14
  100. package/dist/commands/inspect.js.map +1 -1
  101. package/dist/commands/k8s.d.ts.map +1 -1
  102. package/dist/commands/k8s.js +23 -18
  103. package/dist/commands/k8s.js.map +1 -1
  104. package/dist/commands/kb.d.ts.map +1 -1
  105. package/dist/commands/kb.js +88 -29
  106. package/dist/commands/kb.js.map +1 -1
  107. package/dist/commands/list.d.ts.map +1 -1
  108. package/dist/commands/list.js +21 -9
  109. package/dist/commands/list.js.map +1 -1
  110. package/dist/commands/log-health.d.ts +120 -0
  111. package/dist/commands/log-health.d.ts.map +1 -0
  112. package/dist/commands/log-health.js +212 -0
  113. package/dist/commands/log-health.js.map +1 -0
  114. package/dist/commands/logs.d.ts +96 -2
  115. package/dist/commands/logs.d.ts.map +1 -1
  116. package/dist/commands/logs.js +522 -196
  117. package/dist/commands/logs.js.map +1 -1
  118. package/dist/commands/metrics.d.ts.map +1 -1
  119. package/dist/commands/metrics.js +35 -17
  120. package/dist/commands/metrics.js.map +1 -1
  121. package/dist/commands/monit.d.ts.map +1 -1
  122. package/dist/commands/monit.js +3 -2
  123. package/dist/commands/monit.js.map +1 -1
  124. package/dist/commands/node.d.ts +56 -0
  125. package/dist/commands/node.d.ts.map +1 -1
  126. package/dist/commands/node.js +297 -69
  127. package/dist/commands/node.js.map +1 -1
  128. package/dist/commands/output.d.ts +10 -0
  129. package/dist/commands/output.d.ts.map +1 -1
  130. package/dist/commands/output.js +41 -1
  131. package/dist/commands/output.js.map +1 -1
  132. package/dist/commands/pipeline.d.ts.map +1 -1
  133. package/dist/commands/pipeline.js +33 -19
  134. package/dist/commands/pipeline.js.map +1 -1
  135. package/dist/commands/project.d.ts +19 -0
  136. package/dist/commands/project.d.ts.map +1 -1
  137. package/dist/commands/project.js +125 -27
  138. package/dist/commands/project.js.map +1 -1
  139. package/dist/commands/release-store.d.ts +28 -0
  140. package/dist/commands/release-store.d.ts.map +1 -0
  141. package/dist/commands/release-store.js +158 -0
  142. package/dist/commands/release-store.js.map +1 -0
  143. package/dist/commands/release.d.ts +61 -0
  144. package/dist/commands/release.d.ts.map +1 -0
  145. package/dist/commands/release.js +555 -0
  146. package/dist/commands/release.js.map +1 -0
  147. package/dist/commands/reload.d.ts +16 -0
  148. package/dist/commands/reload.d.ts.map +1 -1
  149. package/dist/commands/reload.js +21 -5
  150. package/dist/commands/reload.js.map +1 -1
  151. package/dist/commands/remote.d.ts +40 -0
  152. package/dist/commands/remote.d.ts.map +1 -1
  153. package/dist/commands/remote.js +146 -35
  154. package/dist/commands/remote.js.map +1 -1
  155. package/dist/commands/restart.d.ts +16 -0
  156. package/dist/commands/restart.d.ts.map +1 -1
  157. package/dist/commands/restart.js +21 -5
  158. package/dist/commands/restart.js.map +1 -1
  159. package/dist/commands/scale.d.ts.map +1 -1
  160. package/dist/commands/scale.js +4 -3
  161. package/dist/commands/scale.js.map +1 -1
  162. package/dist/commands/secret.d.ts +54 -0
  163. package/dist/commands/secret.d.ts.map +1 -1
  164. package/dist/commands/secret.js +236 -34
  165. package/dist/commands/secret.js.map +1 -1
  166. package/dist/commands/service-units.d.ts +181 -0
  167. package/dist/commands/service-units.d.ts.map +1 -0
  168. package/dist/commands/service-units.js +291 -0
  169. package/dist/commands/service-units.js.map +1 -0
  170. package/dist/commands/service.d.ts +39 -0
  171. package/dist/commands/service.d.ts.map +1 -0
  172. package/dist/commands/service.js +370 -0
  173. package/dist/commands/service.js.map +1 -0
  174. package/dist/commands/spinner.d.ts +29 -0
  175. package/dist/commands/spinner.d.ts.map +1 -0
  176. package/dist/commands/spinner.js +44 -0
  177. package/dist/commands/spinner.js.map +1 -0
  178. package/dist/commands/stack.d.ts +49 -1
  179. package/dist/commands/stack.d.ts.map +1 -1
  180. package/dist/commands/stack.js +325 -12
  181. package/dist/commands/stack.js.map +1 -1
  182. package/dist/commands/start.d.ts +16 -0
  183. package/dist/commands/start.d.ts.map +1 -1
  184. package/dist/commands/start.js +20 -3
  185. package/dist/commands/start.js.map +1 -1
  186. package/dist/commands/status.d.ts +9 -0
  187. package/dist/commands/status.d.ts.map +1 -1
  188. package/dist/commands/status.js +45 -29
  189. package/dist/commands/status.js.map +1 -1
  190. package/dist/commands/stop.d.ts +16 -0
  191. package/dist/commands/stop.d.ts.map +1 -1
  192. package/dist/commands/stop.js +34 -6
  193. package/dist/commands/stop.js.map +1 -1
  194. package/dist/commands/tor.d.ts +14 -7
  195. package/dist/commands/tor.d.ts.map +1 -1
  196. package/dist/commands/tor.js +113 -33
  197. package/dist/commands/tor.js.map +1 -1
  198. package/dist/commands/up.d.ts +85 -1
  199. package/dist/commands/up.d.ts.map +1 -1
  200. package/dist/commands/up.js +180 -32
  201. package/dist/commands/up.js.map +1 -1
  202. package/dist/commands/webapp.d.ts.map +1 -1
  203. package/dist/commands/webapp.js +2 -1
  204. package/dist/commands/webapp.js.map +1 -1
  205. package/dist/config/defaults.d.ts +16 -0
  206. package/dist/config/defaults.d.ts.map +1 -1
  207. package/dist/config/defaults.js +26 -3
  208. package/dist/config/defaults.js.map +1 -1
  209. package/dist/config/define-ecosystem.d.ts.map +1 -1
  210. package/dist/config/define-ecosystem.js +2 -0
  211. package/dist/config/define-ecosystem.js.map +1 -1
  212. package/dist/config/index.d.ts +1 -1
  213. package/dist/config/index.d.ts.map +1 -1
  214. package/dist/config/index.js +1 -1
  215. package/dist/config/index.js.map +1 -1
  216. package/dist/config/loader.d.ts +45 -1
  217. package/dist/config/loader.d.ts.map +1 -1
  218. package/dist/config/loader.js +64 -6
  219. package/dist/config/loader.js.map +1 -1
  220. package/dist/config/omnitron-config.schema.d.ts.map +1 -1
  221. package/dist/config/omnitron-config.schema.js +16 -0
  222. package/dist/config/omnitron-config.schema.js.map +1 -1
  223. package/dist/config/types.d.ts +416 -44
  224. package/dist/config/types.d.ts.map +1 -1
  225. package/dist/daemon/config-reload.d.ts +27 -0
  226. package/dist/daemon/config-reload.d.ts.map +1 -0
  227. package/dist/daemon/config-reload.js +42 -0
  228. package/dist/daemon/config-reload.js.map +1 -0
  229. package/dist/daemon/daemon-client.d.ts +136 -2
  230. package/dist/daemon/daemon-client.d.ts.map +1 -1
  231. package/dist/daemon/daemon-client.js +307 -21
  232. package/dist/daemon/daemon-client.js.map +1 -1
  233. package/dist/daemon/daemon-entry.js +90 -30
  234. package/dist/daemon/daemon-entry.js.map +1 -1
  235. package/dist/daemon/daemon-scheduler.d.ts +4 -0
  236. package/dist/daemon/daemon-scheduler.d.ts.map +1 -1
  237. package/dist/daemon/daemon-scheduler.js +137 -7
  238. package/dist/daemon/daemon-scheduler.js.map +1 -1
  239. package/dist/daemon/daemon-state-store.service.d.ts +263 -0
  240. package/dist/daemon/daemon-state-store.service.d.ts.map +1 -0
  241. package/dist/daemon/daemon-state-store.service.js +446 -0
  242. package/dist/daemon/daemon-state-store.service.js.map +1 -0
  243. package/dist/daemon/daemon.d.ts +194 -4
  244. package/dist/daemon/daemon.d.ts.map +1 -1
  245. package/dist/daemon/daemon.js +1160 -157
  246. package/dist/daemon/daemon.js.map +1 -1
  247. package/dist/daemon/daemon.module.d.ts.map +1 -1
  248. package/dist/daemon/daemon.module.js +163 -85
  249. package/dist/daemon/daemon.module.js.map +1 -1
  250. package/dist/daemon/daemon.rpc-service.d.ts +90 -1
  251. package/dist/daemon/daemon.rpc-service.d.ts.map +1 -1
  252. package/dist/daemon/daemon.rpc-service.js +439 -97
  253. package/dist/daemon/daemon.rpc-service.js.map +1 -1
  254. package/dist/daemon/pid-manager.d.ts +16 -3
  255. package/dist/daemon/pid-manager.d.ts.map +1 -1
  256. package/dist/daemon/pid-manager.js +31 -4
  257. package/dist/daemon/pid-manager.js.map +1 -1
  258. package/dist/daemon/spawn-daemon.d.ts +73 -0
  259. package/dist/daemon/spawn-daemon.d.ts.map +1 -0
  260. package/dist/daemon/spawn-daemon.js +101 -0
  261. package/dist/daemon/spawn-daemon.js.map +1 -0
  262. package/dist/daemon/state-store.d.ts +65 -3
  263. package/dist/daemon/state-store.d.ts.map +1 -1
  264. package/dist/daemon/state-store.js +100 -58
  265. package/dist/daemon/state-store.js.map +1 -1
  266. package/dist/database/connection.d.ts +71 -0
  267. package/dist/database/connection.d.ts.map +1 -0
  268. package/dist/database/connection.js +129 -0
  269. package/dist/database/connection.js.map +1 -0
  270. package/dist/database/dialect.d.ts +60 -0
  271. package/dist/database/dialect.d.ts.map +1 -0
  272. package/dist/database/dialect.js +80 -0
  273. package/dist/database/dialect.js.map +1 -0
  274. package/dist/database/migrate.d.ts +5 -2
  275. package/dist/database/migrate.d.ts.map +1 -1
  276. package/dist/database/migrate.js +23 -33
  277. package/dist/database/migrate.js.map +1 -1
  278. package/dist/database/migration-runner.d.ts +50 -0
  279. package/dist/database/migration-runner.d.ts.map +1 -0
  280. package/dist/database/migration-runner.js +143 -0
  281. package/dist/database/migration-runner.js.map +1 -0
  282. package/dist/database/migrations/006_login_throttle.d.ts +17 -0
  283. package/dist/database/migrations/006_login_throttle.d.ts.map +1 -0
  284. package/dist/database/migrations/006_login_throttle.js +33 -0
  285. package/dist/database/migrations/006_login_throttle.js.map +1 -0
  286. package/dist/database/migrations/007_sync_ingested.d.ts +21 -0
  287. package/dist/database/migrations/007_sync_ingested.d.ts.map +1 -0
  288. package/dist/database/migrations/007_sync_ingested.js +39 -0
  289. package/dist/database/migrations/007_sync_ingested.js.map +1 -0
  290. package/dist/database/migrations/008_node_health_version_text.d.ts +24 -0
  291. package/dist/database/migrations/008_node_health_version_text.d.ts.map +1 -0
  292. package/dist/database/migrations/008_node_health_version_text.js +35 -0
  293. package/dist/database/migrations/008_node_health_version_text.js.map +1 -0
  294. package/dist/database/migrations/009_node_health_unmeasured.d.ts +40 -0
  295. package/dist/database/migrations/009_node_health_unmeasured.d.ts.map +1 -0
  296. package/dist/database/migrations/009_node_health_unmeasured.js +63 -0
  297. package/dist/database/migrations/009_node_health_unmeasured.js.map +1 -0
  298. package/dist/database/migrations/010_metrics_raw_had_no_reader.d.ts +22 -0
  299. package/dist/database/migrations/010_metrics_raw_had_no_reader.d.ts.map +1 -0
  300. package/dist/database/migrations/010_metrics_raw_had_no_reader.js +26 -0
  301. package/dist/database/migrations/010_metrics_raw_had_no_reader.js.map +1 -0
  302. package/dist/database/migrations/011_alerts_nobody_had_to_write.d.ts +56 -0
  303. package/dist/database/migrations/011_alerts_nobody_had_to_write.d.ts.map +1 -0
  304. package/dist/database/migrations/011_alerts_nobody_had_to_write.js +79 -0
  305. package/dist/database/migrations/011_alerts_nobody_had_to_write.js.map +1 -0
  306. package/dist/database/migrations/012_a_disk_nobody_watched.d.ts +26 -0
  307. package/dist/database/migrations/012_a_disk_nobody_watched.d.ts.map +1 -0
  308. package/dist/database/migrations/012_a_disk_nobody_watched.js +40 -0
  309. package/dist/database/migrations/012_a_disk_nobody_watched.js.map +1 -0
  310. package/dist/database/migrations/013_an_alarm_delivered.d.ts +14 -0
  311. package/dist/database/migrations/013_an_alarm_delivered.d.ts.map +1 -0
  312. package/dist/database/migrations/013_an_alarm_delivered.js +28 -0
  313. package/dist/database/migrations/013_an_alarm_delivered.js.map +1 -0
  314. package/dist/database/migrations/index.d.ts +21 -0
  315. package/dist/database/migrations/index.d.ts.map +1 -0
  316. package/dist/database/migrations/index.js +47 -0
  317. package/dist/database/migrations/index.js.map +1 -0
  318. package/dist/database/schema.d.ts +27 -11
  319. package/dist/database/schema.d.ts.map +1 -1
  320. package/dist/database/sqlite-date-binding.d.ts +71 -0
  321. package/dist/database/sqlite-date-binding.d.ts.map +1 -0
  322. package/dist/database/sqlite-date-binding.js +174 -0
  323. package/dist/database/sqlite-date-binding.js.map +1 -0
  324. package/dist/execution/data-channel.d.ts +48 -0
  325. package/dist/execution/data-channel.d.ts.map +1 -0
  326. package/dist/execution/data-channel.js +55 -0
  327. package/dist/execution/data-channel.js.map +1 -0
  328. package/dist/execution/execution.service.d.ts +146 -0
  329. package/dist/execution/execution.service.d.ts.map +1 -1
  330. package/dist/execution/execution.service.js +330 -10
  331. package/dist/execution/execution.service.js.map +1 -1
  332. package/dist/index.d.ts +1 -1
  333. package/dist/index.d.ts.map +1 -1
  334. package/dist/infrastructure/bare-metal-plan.d.ts +208 -0
  335. package/dist/infrastructure/bare-metal-plan.d.ts.map +1 -0
  336. package/dist/infrastructure/bare-metal-plan.js +363 -0
  337. package/dist/infrastructure/bare-metal-plan.js.map +1 -0
  338. package/dist/infrastructure/bare-metal-runner.d.ts +65 -0
  339. package/dist/infrastructure/bare-metal-runner.d.ts.map +1 -0
  340. package/dist/infrastructure/bare-metal-runner.js +354 -0
  341. package/dist/infrastructure/bare-metal-runner.js.map +1 -0
  342. package/dist/infrastructure/config-payload.d.ts +149 -0
  343. package/dist/infrastructure/config-payload.d.ts.map +1 -0
  344. package/dist/infrastructure/config-payload.js +235 -0
  345. package/dist/infrastructure/config-payload.js.map +1 -0
  346. package/dist/infrastructure/container-runtime.d.ts +170 -10
  347. package/dist/infrastructure/container-runtime.d.ts.map +1 -1
  348. package/dist/infrastructure/container-runtime.js +565 -52
  349. package/dist/infrastructure/container-runtime.js.map +1 -1
  350. package/dist/infrastructure/gateway-reachability.d.ts +85 -0
  351. package/dist/infrastructure/gateway-reachability.d.ts.map +1 -0
  352. package/dist/infrastructure/gateway-reachability.js +116 -0
  353. package/dist/infrastructure/gateway-reachability.js.map +1 -0
  354. package/dist/infrastructure/gateway-redis-db.d.ts +29 -0
  355. package/dist/infrastructure/gateway-redis-db.d.ts.map +1 -0
  356. package/dist/infrastructure/gateway-redis-db.js +32 -0
  357. package/dist/infrastructure/gateway-redis-db.js.map +1 -0
  358. package/dist/infrastructure/host-ingress-config.d.ts +12 -0
  359. package/dist/infrastructure/host-ingress-config.d.ts.map +1 -0
  360. package/dist/infrastructure/host-ingress-config.js +197 -0
  361. package/dist/infrastructure/host-ingress-config.js.map +1 -0
  362. package/dist/infrastructure/host-ingress-lock.d.ts +4 -0
  363. package/dist/infrastructure/host-ingress-lock.d.ts.map +1 -0
  364. package/dist/infrastructure/host-ingress-lock.js +36 -0
  365. package/dist/infrastructure/host-ingress-lock.js.map +1 -0
  366. package/dist/infrastructure/host-ingress-runtime.d.ts +25 -0
  367. package/dist/infrastructure/host-ingress-runtime.d.ts.map +1 -0
  368. package/dist/infrastructure/host-ingress-runtime.js +114 -0
  369. package/dist/infrastructure/host-ingress-runtime.js.map +1 -0
  370. package/dist/infrastructure/host-ingress-takeover.d.ts +18 -0
  371. package/dist/infrastructure/host-ingress-takeover.d.ts.map +1 -0
  372. package/dist/infrastructure/host-ingress-takeover.js +142 -0
  373. package/dist/infrastructure/host-ingress-takeover.js.map +1 -0
  374. package/dist/infrastructure/host-ingress-types.d.ts +173 -0
  375. package/dist/infrastructure/host-ingress-types.d.ts.map +1 -0
  376. package/dist/infrastructure/host-ingress-types.js +2 -0
  377. package/dist/infrastructure/host-ingress-types.js.map +1 -0
  378. package/dist/infrastructure/host-ingress.service.d.ts +47 -0
  379. package/dist/infrastructure/host-ingress.service.d.ts.map +1 -0
  380. package/dist/infrastructure/host-ingress.service.js +514 -0
  381. package/dist/infrastructure/host-ingress.service.js.map +1 -0
  382. package/dist/infrastructure/host-inspection.d.ts +223 -0
  383. package/dist/infrastructure/host-inspection.d.ts.map +1 -0
  384. package/dist/infrastructure/host-inspection.js +501 -0
  385. package/dist/infrastructure/host-inspection.js.map +1 -0
  386. package/dist/infrastructure/infrastructure-gate.d.ts.map +1 -1
  387. package/dist/infrastructure/infrastructure.service.d.ts +223 -7
  388. package/dist/infrastructure/infrastructure.service.d.ts.map +1 -1
  389. package/dist/infrastructure/infrastructure.service.js +500 -93
  390. package/dist/infrastructure/infrastructure.service.js.map +1 -1
  391. package/dist/infrastructure/known-machines.d.ts +76 -0
  392. package/dist/infrastructure/known-machines.d.ts.map +1 -0
  393. package/dist/infrastructure/known-machines.js +94 -0
  394. package/dist/infrastructure/known-machines.js.map +1 -0
  395. package/dist/infrastructure/node-credentials.d.ts +45 -0
  396. package/dist/infrastructure/node-credentials.d.ts.map +1 -0
  397. package/dist/infrastructure/node-credentials.js +80 -0
  398. package/dist/infrastructure/node-credentials.js.map +1 -0
  399. package/dist/infrastructure/orphan-containers.d.ts +99 -0
  400. package/dist/infrastructure/orphan-containers.d.ts.map +1 -0
  401. package/dist/infrastructure/orphan-containers.js +106 -0
  402. package/dist/infrastructure/orphan-containers.js.map +1 -0
  403. package/dist/infrastructure/phantom-endpoint-janitor.d.ts +20 -0
  404. package/dist/infrastructure/phantom-endpoint-janitor.d.ts.map +1 -1
  405. package/dist/infrastructure/phantom-endpoint-janitor.js +56 -6
  406. package/dist/infrastructure/phantom-endpoint-janitor.js.map +1 -1
  407. package/dist/infrastructure/presets/gateway-openresty.d.ts.map +1 -1
  408. package/dist/infrastructure/presets/gateway-openresty.js +29 -3
  409. package/dist/infrastructure/presets/gateway-openresty.js.map +1 -1
  410. package/dist/infrastructure/presets/minio.d.ts.map +1 -1
  411. package/dist/infrastructure/presets/minio.js +36 -3
  412. package/dist/infrastructure/presets/minio.js.map +1 -1
  413. package/dist/infrastructure/presets/postgres.d.ts.map +1 -1
  414. package/dist/infrastructure/presets/postgres.js +69 -2
  415. package/dist/infrastructure/presets/postgres.js.map +1 -1
  416. package/dist/infrastructure/presets/redis.d.ts.map +1 -1
  417. package/dist/infrastructure/presets/redis.js +13 -0
  418. package/dist/infrastructure/presets/redis.js.map +1 -1
  419. package/dist/infrastructure/presets/registry.d.ts.map +1 -1
  420. package/dist/infrastructure/presets/registry.js +81 -12
  421. package/dist/infrastructure/presets/registry.js.map +1 -1
  422. package/dist/infrastructure/presets/tor.d.ts +38 -0
  423. package/dist/infrastructure/presets/tor.d.ts.map +1 -1
  424. package/dist/infrastructure/presets/tor.js +78 -69
  425. package/dist/infrastructure/presets/tor.js.map +1 -1
  426. package/dist/infrastructure/presets/torrc.d.ts +54 -0
  427. package/dist/infrastructure/presets/torrc.d.ts.map +1 -0
  428. package/dist/infrastructure/presets/torrc.js +153 -0
  429. package/dist/infrastructure/presets/torrc.js.map +1 -0
  430. package/dist/infrastructure/presets/types.d.ts +17 -0
  431. package/dist/infrastructure/presets/types.d.ts.map +1 -1
  432. package/dist/infrastructure/provisioning-outcome.d.ts +85 -0
  433. package/dist/infrastructure/provisioning-outcome.d.ts.map +1 -0
  434. package/dist/infrastructure/provisioning-outcome.js +122 -0
  435. package/dist/infrastructure/provisioning-outcome.js.map +1 -0
  436. package/dist/infrastructure/server-registry.d.ts +27 -0
  437. package/dist/infrastructure/server-registry.d.ts.map +1 -1
  438. package/dist/infrastructure/server-registry.js +60 -5
  439. package/dist/infrastructure/server-registry.js.map +1 -1
  440. package/dist/infrastructure/service-binding.d.ts +60 -0
  441. package/dist/infrastructure/service-binding.d.ts.map +1 -0
  442. package/dist/infrastructure/service-binding.js +90 -0
  443. package/dist/infrastructure/service-binding.js.map +1 -0
  444. package/dist/infrastructure/service-credentials.d.ts +105 -0
  445. package/dist/infrastructure/service-credentials.d.ts.map +1 -0
  446. package/dist/infrastructure/service-credentials.js +141 -0
  447. package/dist/infrastructure/service-credentials.js.map +1 -0
  448. package/dist/infrastructure/service-resolver.d.ts +166 -12
  449. package/dist/infrastructure/service-resolver.d.ts.map +1 -1
  450. package/dist/infrastructure/service-resolver.js +535 -82
  451. package/dist/infrastructure/service-resolver.js.map +1 -1
  452. package/dist/infrastructure/shipped-config.d.ts +43 -0
  453. package/dist/infrastructure/shipped-config.d.ts.map +1 -0
  454. package/dist/infrastructure/shipped-config.js +159 -0
  455. package/dist/infrastructure/shipped-config.js.map +1 -0
  456. package/dist/infrastructure/stack-infra-manager.d.ts +16 -1
  457. package/dist/infrastructure/stack-infra-manager.d.ts.map +1 -1
  458. package/dist/infrastructure/stack-infra-manager.js +81 -53
  459. package/dist/infrastructure/stack-infra-manager.js.map +1 -1
  460. package/dist/infrastructure/types.d.ts +263 -6
  461. package/dist/infrastructure/types.d.ts.map +1 -1
  462. package/dist/mcp/mcp-bridge.d.ts.map +1 -1
  463. package/dist/mcp/mcp-bridge.js.map +1 -1
  464. package/dist/mcp/tool-groups/apps.tools.d.ts +2 -1
  465. package/dist/mcp/tool-groups/apps.tools.d.ts.map +1 -1
  466. package/dist/mcp/tool-groups/apps.tools.js +2 -2
  467. package/dist/mcp/tool-groups/apps.tools.js.map +1 -1
  468. package/dist/mcp/tool-groups/infra.tools.d.ts +16 -2
  469. package/dist/mcp/tool-groups/infra.tools.d.ts.map +1 -1
  470. package/dist/mcp/tool-groups/infra.tools.js +51 -41
  471. package/dist/mcp/tool-groups/infra.tools.js.map +1 -1
  472. package/dist/mcp/tool-groups/kb.tools.d.ts.map +1 -1
  473. package/dist/mcp/tool-groups/kb.tools.js +4 -6
  474. package/dist/mcp/tool-groups/kb.tools.js.map +1 -1
  475. package/dist/mcp/tool-groups/management.tools.d.ts +20 -3
  476. package/dist/mcp/tool-groups/management.tools.d.ts.map +1 -1
  477. package/dist/mcp/tool-groups/management.tools.js +158 -125
  478. package/dist/mcp/tool-groups/management.tools.js.map +1 -1
  479. package/dist/mcp/tool-groups/monitoring.tools.d.ts +10 -1
  480. package/dist/mcp/tool-groups/monitoring.tools.d.ts.map +1 -1
  481. package/dist/mcp/tool-groups/monitoring.tools.js +65 -18
  482. package/dist/mcp/tool-groups/monitoring.tools.js.map +1 -1
  483. package/dist/mcp/unavailable-tools.d.ts +35 -0
  484. package/dist/mcp/unavailable-tools.d.ts.map +1 -0
  485. package/dist/mcp/unavailable-tools.js +59 -0
  486. package/dist/mcp/unavailable-tools.js.map +1 -0
  487. package/dist/monitoring/app-health.indicator.d.ts.map +1 -1
  488. package/dist/monitoring/app-health.indicator.js +12 -0
  489. package/dist/monitoring/app-health.indicator.js.map +1 -1
  490. package/dist/monitoring/desktop-notifier.d.ts +26 -0
  491. package/dist/monitoring/desktop-notifier.d.ts.map +1 -0
  492. package/dist/monitoring/desktop-notifier.js +49 -0
  493. package/dist/monitoring/desktop-notifier.js.map +1 -0
  494. package/dist/monitoring/docker-health.indicator.d.ts +17 -1
  495. package/dist/monitoring/docker-health.indicator.d.ts.map +1 -1
  496. package/dist/monitoring/docker-health.indicator.js +38 -8
  497. package/dist/monitoring/docker-health.indicator.js.map +1 -1
  498. package/dist/monitoring/event-loop-stall.indicator.d.ts +26 -0
  499. package/dist/monitoring/event-loop-stall.indicator.d.ts.map +1 -0
  500. package/dist/monitoring/event-loop-stall.indicator.js +47 -0
  501. package/dist/monitoring/event-loop-stall.indicator.js.map +1 -0
  502. package/dist/monitoring/event-loop-watch.d.ts +124 -0
  503. package/dist/monitoring/event-loop-watch.d.ts.map +1 -0
  504. package/dist/monitoring/event-loop-watch.js +179 -0
  505. package/dist/monitoring/event-loop-watch.js.map +1 -0
  506. package/dist/monitoring/host-disk.d.ts +6 -0
  507. package/dist/monitoring/host-disk.d.ts.map +1 -0
  508. package/dist/monitoring/host-disk.js +26 -0
  509. package/dist/monitoring/host-disk.js.map +1 -0
  510. package/dist/monitoring/log-line.d.ts +188 -0
  511. package/dist/monitoring/log-line.d.ts.map +1 -0
  512. package/dist/monitoring/log-line.js +458 -0
  513. package/dist/monitoring/log-line.js.map +1 -0
  514. package/dist/monitoring/log-manager.d.ts +89 -8
  515. package/dist/monitoring/log-manager.d.ts.map +1 -1
  516. package/dist/monitoring/log-manager.js +246 -77
  517. package/dist/monitoring/log-manager.js.map +1 -1
  518. package/dist/observability/metrics-bridge.d.ts.map +1 -1
  519. package/dist/observability/metrics-server.d.ts.map +1 -1
  520. package/dist/operations.d.ts +16 -0
  521. package/dist/operations.d.ts.map +1 -0
  522. package/dist/operations.js +13 -0
  523. package/dist/operations.js.map +1 -0
  524. package/dist/orchestrator/app-handle.d.ts +157 -2
  525. package/dist/orchestrator/app-handle.d.ts.map +1 -1
  526. package/dist/orchestrator/app-handle.js +165 -3
  527. package/dist/orchestrator/app-handle.js.map +1 -1
  528. package/dist/orchestrator/app-health.d.ts +33 -0
  529. package/dist/orchestrator/app-health.d.ts.map +1 -0
  530. package/dist/orchestrator/app-health.js +74 -0
  531. package/dist/orchestrator/app-health.js.map +1 -0
  532. package/dist/orchestrator/app-traffic.d.ts +14 -0
  533. package/dist/orchestrator/app-traffic.d.ts.map +1 -0
  534. package/dist/orchestrator/app-traffic.js +33 -0
  535. package/dist/orchestrator/app-traffic.js.map +1 -0
  536. package/dist/orchestrator/bootstrap-loader-child.d.ts +19 -0
  537. package/dist/orchestrator/bootstrap-loader-child.d.ts.map +1 -0
  538. package/dist/orchestrator/bootstrap-loader-child.js +43 -0
  539. package/dist/orchestrator/bootstrap-loader-child.js.map +1 -0
  540. package/dist/orchestrator/bootstrap-loader.d.ts +72 -0
  541. package/dist/orchestrator/bootstrap-loader.d.ts.map +1 -1
  542. package/dist/orchestrator/bootstrap-loader.js +350 -5
  543. package/dist/orchestrator/bootstrap-loader.js.map +1 -1
  544. package/dist/orchestrator/bootstrap-process.d.ts +99 -4
  545. package/dist/orchestrator/bootstrap-process.d.ts.map +1 -1
  546. package/dist/orchestrator/bootstrap-process.js +332 -20
  547. package/dist/orchestrator/bootstrap-process.js.map +1 -1
  548. package/dist/orchestrator/build-service.d.ts +6 -12
  549. package/dist/orchestrator/build-service.d.ts.map +1 -1
  550. package/dist/orchestrator/build-service.js +120 -83
  551. package/dist/orchestrator/build-service.js.map +1 -1
  552. package/dist/orchestrator/call-exposed-service.d.ts +7 -0
  553. package/dist/orchestrator/call-exposed-service.d.ts.map +1 -0
  554. package/dist/orchestrator/call-exposed-service.js +50 -0
  555. package/dist/orchestrator/call-exposed-service.js.map +1 -0
  556. package/dist/orchestrator/classic-launcher.d.ts +1 -0
  557. package/dist/orchestrator/classic-launcher.d.ts.map +1 -1
  558. package/dist/orchestrator/classic-launcher.js +40 -4
  559. package/dist/orchestrator/classic-launcher.js.map +1 -1
  560. package/dist/orchestrator/file-watcher.d.ts +79 -1
  561. package/dist/orchestrator/file-watcher.d.ts.map +1 -1
  562. package/dist/orchestrator/file-watcher.js +193 -7
  563. package/dist/orchestrator/file-watcher.js.map +1 -1
  564. package/dist/orchestrator/http-routes.d.ts +4 -0
  565. package/dist/orchestrator/http-routes.d.ts.map +1 -0
  566. package/dist/orchestrator/http-routes.js +19 -0
  567. package/dist/orchestrator/http-routes.js.map +1 -0
  568. package/dist/orchestrator/module-worker-process.d.ts.map +1 -1
  569. package/dist/orchestrator/module-worker-process.js +10 -0
  570. package/dist/orchestrator/module-worker-process.js.map +1 -1
  571. package/dist/orchestrator/orchestrator.service.d.ts +385 -11
  572. package/dist/orchestrator/orchestrator.service.d.ts.map +1 -1
  573. package/dist/orchestrator/orchestrator.service.js +1738 -364
  574. package/dist/orchestrator/orchestrator.service.js.map +1 -1
  575. package/dist/orchestrator/owned-pids.d.ts +83 -0
  576. package/dist/orchestrator/owned-pids.d.ts.map +1 -0
  577. package/dist/orchestrator/owned-pids.js +93 -0
  578. package/dist/orchestrator/owned-pids.js.map +1 -0
  579. package/dist/orchestrator/port-conflict.d.ts +102 -0
  580. package/dist/orchestrator/port-conflict.d.ts.map +1 -0
  581. package/dist/orchestrator/port-conflict.js +131 -0
  582. package/dist/orchestrator/port-conflict.js.map +1 -0
  583. package/dist/orchestrator/process-janitor.d.ts +78 -4
  584. package/dist/orchestrator/process-janitor.d.ts.map +1 -1
  585. package/dist/orchestrator/process-janitor.js +246 -30
  586. package/dist/orchestrator/process-janitor.js.map +1 -1
  587. package/dist/orchestrator/ps-metrics.d.ts +37 -0
  588. package/dist/orchestrator/ps-metrics.d.ts.map +1 -0
  589. package/dist/orchestrator/ps-metrics.js +60 -0
  590. package/dist/orchestrator/ps-metrics.js.map +1 -0
  591. package/dist/orchestrator/service-router.d.ts +58 -2
  592. package/dist/orchestrator/service-router.d.ts.map +1 -1
  593. package/dist/orchestrator/service-router.js +157 -25
  594. package/dist/orchestrator/service-router.js.map +1 -1
  595. package/dist/orchestrator/topology-query.d.ts +49 -0
  596. package/dist/orchestrator/topology-query.d.ts.map +1 -0
  597. package/dist/orchestrator/topology-query.js +54 -0
  598. package/dist/orchestrator/topology-query.js.map +1 -0
  599. package/dist/project/artifact-builder.d.ts +174 -3
  600. package/dist/project/artifact-builder.d.ts.map +1 -1
  601. package/dist/project/artifact-builder.js +454 -42
  602. package/dist/project/artifact-builder.js.map +1 -1
  603. package/dist/project/build-decision.d.ts +58 -0
  604. package/dist/project/build-decision.d.ts.map +1 -0
  605. package/dist/project/build-decision.js +59 -0
  606. package/dist/project/build-decision.js.map +1 -0
  607. package/dist/project/config-resolver.d.ts +6 -4
  608. package/dist/project/config-resolver.d.ts.map +1 -1
  609. package/dist/project/config-resolver.js +128 -74
  610. package/dist/project/config-resolver.js.map +1 -1
  611. package/dist/project/declared-config.d.ts +42 -0
  612. package/dist/project/declared-config.d.ts.map +1 -0
  613. package/dist/project/declared-config.js +60 -0
  614. package/dist/project/declared-config.js.map +1 -0
  615. package/dist/project/dependency-analyzer.d.ts.map +1 -1
  616. package/dist/project/dependency-analyzer.js.map +1 -1
  617. package/dist/project/deploy-phases.d.ts +35 -0
  618. package/dist/project/deploy-phases.d.ts.map +1 -0
  619. package/dist/project/deploy-phases.js +75 -0
  620. package/dist/project/deploy-phases.js.map +1 -0
  621. package/dist/project/exec-in-group.d.ts +49 -0
  622. package/dist/project/exec-in-group.d.ts.map +1 -0
  623. package/dist/project/exec-in-group.js +100 -0
  624. package/dist/project/exec-in-group.js.map +1 -0
  625. package/dist/project/node-app-config.d.ts +101 -0
  626. package/dist/project/node-app-config.d.ts.map +1 -0
  627. package/dist/project/node-app-config.js +134 -0
  628. package/dist/project/node-app-config.js.map +1 -0
  629. package/dist/project/node-app-health.d.ts +76 -0
  630. package/dist/project/node-app-health.d.ts.map +1 -0
  631. package/dist/project/node-app-health.js +119 -0
  632. package/dist/project/node-app-health.js.map +1 -0
  633. package/dist/project/operator-account.d.ts +293 -0
  634. package/dist/project/operator-account.d.ts.map +1 -0
  635. package/dist/project/operator-account.js +373 -0
  636. package/dist/project/operator-account.js.map +1 -0
  637. package/dist/project/registry.d.ts +45 -8
  638. package/dist/project/registry.d.ts.map +1 -1
  639. package/dist/project/registry.js +214 -64
  640. package/dist/project/registry.js.map +1 -1
  641. package/dist/project/requirements-scanner.d.ts +1 -0
  642. package/dist/project/requirements-scanner.d.ts.map +1 -1
  643. package/dist/project/requirements-scanner.js +28 -3
  644. package/dist/project/requirements-scanner.js.map +1 -1
  645. package/dist/project/stack-env.d.ts +37 -0
  646. package/dist/project/stack-env.d.ts.map +1 -0
  647. package/dist/project/stack-env.js +85 -0
  648. package/dist/project/stack-env.js.map +1 -0
  649. package/dist/project/stand-containers.d.ts +11 -0
  650. package/dist/project/stand-containers.d.ts.map +1 -0
  651. package/dist/project/stand-containers.js +27 -0
  652. package/dist/project/stand-containers.js.map +1 -0
  653. package/dist/project/working-tree.d.ts +62 -0
  654. package/dist/project/working-tree.d.ts.map +1 -0
  655. package/dist/project/working-tree.js +101 -0
  656. package/dist/project/working-tree.js.map +1 -0
  657. package/dist/release/artifact-store.d.ts +360 -0
  658. package/dist/release/artifact-store.d.ts.map +1 -0
  659. package/dist/release/artifact-store.js +1040 -0
  660. package/dist/release/artifact-store.js.map +1 -0
  661. package/dist/release/attest-on-node.d.ts +93 -0
  662. package/dist/release/attest-on-node.d.ts.map +1 -0
  663. package/dist/release/attest-on-node.js +207 -0
  664. package/dist/release/attest-on-node.js.map +1 -0
  665. package/dist/release/attest.d.ts +135 -0
  666. package/dist/release/attest.d.ts.map +1 -0
  667. package/dist/release/attest.js +199 -0
  668. package/dist/release/attest.js.map +1 -0
  669. package/dist/release/build-run.d.ts +151 -0
  670. package/dist/release/build-run.d.ts.map +1 -0
  671. package/dist/release/build-run.js +609 -0
  672. package/dist/release/build-run.js.map +1 -0
  673. package/dist/release/builder.d.ts +57 -0
  674. package/dist/release/builder.d.ts.map +1 -0
  675. package/dist/release/builder.js +120 -0
  676. package/dist/release/builder.js.map +1 -0
  677. package/dist/release/daemon-omni.d.ts +41 -0
  678. package/dist/release/daemon-omni.d.ts.map +1 -0
  679. package/dist/release/daemon-omni.js +59 -0
  680. package/dist/release/daemon-omni.js.map +1 -0
  681. package/dist/release/definition-inputs.d.ts +34 -0
  682. package/dist/release/definition-inputs.d.ts.map +1 -0
  683. package/dist/release/definition-inputs.js +90 -0
  684. package/dist/release/definition-inputs.js.map +1 -0
  685. package/dist/release/delivered.d.ts +53 -0
  686. package/dist/release/delivered.d.ts.map +1 -0
  687. package/dist/release/delivered.js +81 -0
  688. package/dist/release/delivered.js.map +1 -0
  689. package/dist/release/layout.d.ts +61 -0
  690. package/dist/release/layout.d.ts.map +1 -0
  691. package/dist/release/layout.js +114 -0
  692. package/dist/release/layout.js.map +1 -0
  693. package/dist/release/load.d.ts +44 -0
  694. package/dist/release/load.d.ts.map +1 -0
  695. package/dist/release/load.js +84 -0
  696. package/dist/release/load.js.map +1 -0
  697. package/dist/release/manifest.d.ts +299 -0
  698. package/dist/release/manifest.d.ts.map +1 -0
  699. package/dist/release/manifest.js +176 -0
  700. package/dist/release/manifest.js.map +1 -0
  701. package/dist/release/migrations.d.ts +32 -0
  702. package/dist/release/migrations.d.ts.map +1 -0
  703. package/dist/release/migrations.js +51 -0
  704. package/dist/release/migrations.js.map +1 -0
  705. package/dist/release/publish.d.ts +71 -0
  706. package/dist/release/publish.d.ts.map +1 -0
  707. package/dist/release/publish.js +102 -0
  708. package/dist/release/publish.js.map +1 -0
  709. package/dist/release/store.d.ts +174 -0
  710. package/dist/release/store.d.ts.map +1 -0
  711. package/dist/release/store.js +257 -0
  712. package/dist/release/store.js.map +1 -0
  713. package/dist/services/alert.rpc-service.d.ts +11 -15
  714. package/dist/services/alert.rpc-service.d.ts.map +1 -1
  715. package/dist/services/alert.rpc-service.js +29 -14
  716. package/dist/services/alert.rpc-service.js.map +1 -1
  717. package/dist/services/alert.service.d.ts +96 -59
  718. package/dist/services/alert.service.d.ts.map +1 -1
  719. package/dist/services/alert.service.js +455 -70
  720. package/dist/services/alert.service.js.map +1 -1
  721. package/dist/services/anonymous-input.d.ts +26 -0
  722. package/dist/services/anonymous-input.d.ts.map +1 -0
  723. package/dist/services/anonymous-input.js +44 -0
  724. package/dist/services/anonymous-input.js.map +1 -0
  725. package/dist/services/audit.rpc-service.d.ts +27 -0
  726. package/dist/services/audit.rpc-service.d.ts.map +1 -0
  727. package/dist/services/audit.rpc-service.js +57 -0
  728. package/dist/services/audit.rpc-service.js.map +1 -0
  729. package/dist/services/audit.service.d.ts +165 -0
  730. package/dist/services/audit.service.d.ts.map +1 -0
  731. package/dist/services/audit.service.js +242 -0
  732. package/dist/services/audit.service.js.map +1 -0
  733. package/dist/services/auth-context.d.ts +21 -1
  734. package/dist/services/auth-context.d.ts.map +1 -1
  735. package/dist/services/auth-context.js +51 -5
  736. package/dist/services/auth-context.js.map +1 -1
  737. package/dist/services/auth.rpc-service.d.ts +0 -1
  738. package/dist/services/auth.rpc-service.d.ts.map +1 -1
  739. package/dist/services/auth.rpc-service.js +23 -9
  740. package/dist/services/auth.rpc-service.js.map +1 -1
  741. package/dist/services/auth.service.d.ts +47 -33
  742. package/dist/services/auth.service.d.ts.map +1 -1
  743. package/dist/services/auth.service.js +155 -23
  744. package/dist/services/auth.service.js.map +1 -1
  745. package/dist/services/backup-pipeline.d.ts +97 -0
  746. package/dist/services/backup-pipeline.d.ts.map +1 -0
  747. package/dist/services/backup-pipeline.js +286 -0
  748. package/dist/services/backup-pipeline.js.map +1 -0
  749. package/dist/services/backup-schedule.d.ts +69 -0
  750. package/dist/services/backup-schedule.d.ts.map +1 -0
  751. package/dist/services/backup-schedule.js +132 -0
  752. package/dist/services/backup-schedule.js.map +1 -0
  753. package/dist/services/backup.rpc-service.d.ts +31 -2
  754. package/dist/services/backup.rpc-service.d.ts.map +1 -1
  755. package/dist/services/backup.rpc-service.js +53 -0
  756. package/dist/services/backup.rpc-service.js.map +1 -1
  757. package/dist/services/backup.service.d.ts +458 -14
  758. package/dist/services/backup.service.d.ts.map +1 -1
  759. package/dist/services/backup.service.js +1368 -170
  760. package/dist/services/backup.service.js.map +1 -1
  761. package/dist/services/bundle-build-worker.d.ts +19 -0
  762. package/dist/services/bundle-build-worker.d.ts.map +1 -0
  763. package/dist/services/bundle-build-worker.js +40 -0
  764. package/dist/services/bundle-build-worker.js.map +1 -0
  765. package/dist/services/bundle-builder.d.ts +490 -0
  766. package/dist/services/bundle-builder.d.ts.map +1 -0
  767. package/dist/services/bundle-builder.js +1159 -0
  768. package/dist/services/bundle-builder.js.map +1 -0
  769. package/dist/services/bundle-worker-protocol.d.ts +19 -0
  770. package/dist/services/bundle-worker-protocol.d.ts.map +1 -0
  771. package/dist/services/bundle-worker-protocol.js +32 -0
  772. package/dist/services/bundle-worker-protocol.js.map +1 -0
  773. package/dist/services/deploy.rpc-service.d.ts +4 -1
  774. package/dist/services/deploy.rpc-service.d.ts.map +1 -1
  775. package/dist/services/deploy.rpc-service.js +10 -0
  776. package/dist/services/deploy.rpc-service.js.map +1 -1
  777. package/dist/services/deploy.service.d.ts +14 -24
  778. package/dist/services/deploy.service.d.ts.map +1 -1
  779. package/dist/services/deploy.service.js +46 -5
  780. package/dist/services/deploy.service.js.map +1 -1
  781. package/dist/services/discovery.rpc-service.d.ts +2 -1
  782. package/dist/services/discovery.rpc-service.d.ts.map +1 -1
  783. package/dist/services/discovery.rpc-service.js.map +1 -1
  784. package/dist/services/discovery.service.d.ts +32 -28
  785. package/dist/services/discovery.service.d.ts.map +1 -1
  786. package/dist/services/discovery.service.js +108 -133
  787. package/dist/services/discovery.service.js.map +1 -1
  788. package/dist/services/event-broadcaster.rpc-service.d.ts +36 -23
  789. package/dist/services/event-broadcaster.rpc-service.d.ts.map +1 -1
  790. package/dist/services/event-broadcaster.rpc-service.js +70 -32
  791. package/dist/services/event-broadcaster.rpc-service.js.map +1 -1
  792. package/dist/services/event-broadcaster.service.d.ts.map +1 -1
  793. package/dist/services/event-broadcaster.service.js.map +1 -1
  794. package/dist/services/fleet.rpc-service.d.ts +2 -1
  795. package/dist/services/fleet.rpc-service.d.ts.map +1 -1
  796. package/dist/services/fleet.rpc-service.js +3 -1
  797. package/dist/services/fleet.rpc-service.js.map +1 -1
  798. package/dist/services/fleet.service.d.ts +32 -34
  799. package/dist/services/fleet.service.d.ts.map +1 -1
  800. package/dist/services/fleet.service.js +66 -10
  801. package/dist/services/fleet.service.js.map +1 -1
  802. package/dist/services/health-check.rpc-service.d.ts +2 -1
  803. package/dist/services/health-check.rpc-service.d.ts.map +1 -1
  804. package/dist/services/health-check.rpc-service.js.map +1 -1
  805. package/dist/services/health-check.service.d.ts +50 -36
  806. package/dist/services/health-check.service.d.ts.map +1 -1
  807. package/dist/services/health-check.service.js +179 -182
  808. package/dist/services/health-check.service.js.map +1 -1
  809. package/dist/services/host-operation-barrier.d.ts +8 -0
  810. package/dist/services/host-operation-barrier.d.ts.map +1 -0
  811. package/dist/services/host-operation-barrier.js +44 -0
  812. package/dist/services/host-operation-barrier.js.map +1 -0
  813. package/dist/services/host-upgrade.d.ts +129 -0
  814. package/dist/services/host-upgrade.d.ts.map +1 -0
  815. package/dist/services/host-upgrade.js +352 -0
  816. package/dist/services/host-upgrade.js.map +1 -0
  817. package/dist/services/infrastructure.rpc-service.d.ts +312 -2
  818. package/dist/services/infrastructure.rpc-service.d.ts.map +1 -1
  819. package/dist/services/infrastructure.rpc-service.js +584 -11
  820. package/dist/services/infrastructure.rpc-service.js.map +1 -1
  821. package/dist/services/kubernetes.rpc-service.d.ts +2 -1
  822. package/dist/services/kubernetes.rpc-service.d.ts.map +1 -1
  823. package/dist/services/kubernetes.rpc-service.js.map +1 -1
  824. package/dist/services/kubernetes.service.d.ts +5 -39
  825. package/dist/services/kubernetes.service.d.ts.map +1 -1
  826. package/dist/services/kubernetes.service.js +29 -5
  827. package/dist/services/kubernetes.service.js.map +1 -1
  828. package/dist/services/local-bundle.d.ts +160 -0
  829. package/dist/services/local-bundle.d.ts.map +1 -0
  830. package/dist/services/local-bundle.js +242 -0
  831. package/dist/services/local-bundle.js.map +1 -0
  832. package/dist/services/lockout.d.ts +28 -0
  833. package/dist/services/lockout.d.ts.map +1 -0
  834. package/dist/services/lockout.js +41 -0
  835. package/dist/services/lockout.js.map +1 -0
  836. package/dist/services/log-collector.rpc-service.d.ts +16 -0
  837. package/dist/services/log-collector.rpc-service.d.ts.map +1 -1
  838. package/dist/services/log-collector.rpc-service.js +16 -0
  839. package/dist/services/log-collector.rpc-service.js.map +1 -1
  840. package/dist/services/log-collector.service.d.ts +172 -54
  841. package/dist/services/log-collector.service.d.ts.map +1 -1
  842. package/dist/services/log-collector.service.js +492 -102
  843. package/dist/services/log-collector.service.js.map +1 -1
  844. package/dist/services/log-retention.d.ts +50 -0
  845. package/dist/services/log-retention.d.ts.map +1 -0
  846. package/dist/services/log-retention.js +52 -0
  847. package/dist/services/log-retention.js.map +1 -0
  848. package/dist/services/maintenance-lock.d.ts +162 -0
  849. package/dist/services/maintenance-lock.d.ts.map +1 -0
  850. package/dist/services/maintenance-lock.js +316 -0
  851. package/dist/services/maintenance-lock.js.map +1 -0
  852. package/dist/services/master-address.d.ts +107 -0
  853. package/dist/services/master-address.d.ts.map +1 -0
  854. package/dist/services/master-address.js +154 -0
  855. package/dist/services/master-address.js.map +1 -0
  856. package/dist/services/migration-output.d.ts +16 -0
  857. package/dist/services/migration-output.d.ts.map +1 -0
  858. package/dist/services/migration-output.js +22 -0
  859. package/dist/services/migration-output.js.map +1 -0
  860. package/dist/services/netron-telemetry-transport.d.ts.map +1 -1
  861. package/dist/services/netron-telemetry-transport.js.map +1 -1
  862. package/dist/services/node-deploy-lease.d.ts +171 -0
  863. package/dist/services/node-deploy-lease.d.ts.map +1 -0
  864. package/dist/services/node-deploy-lease.js +460 -0
  865. package/dist/services/node-deploy-lease.js.map +1 -0
  866. package/dist/services/node-health.repository.d.ts +63 -4
  867. package/dist/services/node-health.repository.d.ts.map +1 -1
  868. package/dist/services/node-health.repository.js +108 -67
  869. package/dist/services/node-health.repository.js.map +1 -1
  870. package/dist/services/node-manager.rpc-service.d.ts +360 -10
  871. package/dist/services/node-manager.rpc-service.d.ts.map +1 -1
  872. package/dist/services/node-manager.rpc-service.js +754 -34
  873. package/dist/services/node-manager.rpc-service.js.map +1 -1
  874. package/dist/services/node-manager.service.d.ts +267 -95
  875. package/dist/services/node-manager.service.d.ts.map +1 -1
  876. package/dist/services/node-manager.service.js +805 -103
  877. package/dist/services/node-manager.service.js.map +1 -1
  878. package/dist/services/node-upgrade.d.ts +129 -0
  879. package/dist/services/node-upgrade.d.ts.map +1 -0
  880. package/dist/services/node-upgrade.js +161 -0
  881. package/dist/services/node-upgrade.js.map +1 -0
  882. package/dist/services/node-upgrade.service.d.ts +232 -0
  883. package/dist/services/node-upgrade.service.d.ts.map +1 -0
  884. package/dist/services/node-upgrade.service.js +468 -0
  885. package/dist/services/node-upgrade.service.js.map +1 -0
  886. package/dist/services/pipeline.rpc-service.d.ts +6 -1
  887. package/dist/services/pipeline.rpc-service.d.ts.map +1 -1
  888. package/dist/services/pipeline.rpc-service.js +9 -0
  889. package/dist/services/pipeline.rpc-service.js.map +1 -1
  890. package/dist/services/pipeline.service.d.ts +17 -46
  891. package/dist/services/pipeline.service.d.ts.map +1 -1
  892. package/dist/services/pipeline.service.js +84 -9
  893. package/dist/services/pipeline.service.js.map +1 -1
  894. package/dist/services/project.rpc-service.d.ts +140 -5
  895. package/dist/services/project.rpc-service.d.ts.map +1 -1
  896. package/dist/services/project.rpc-service.js +260 -22
  897. package/dist/services/project.rpc-service.js.map +1 -1
  898. package/dist/services/project.service.d.ts +687 -15
  899. package/dist/services/project.service.d.ts.map +1 -1
  900. package/dist/services/project.service.js +2846 -337
  901. package/dist/services/project.service.js.map +1 -1
  902. package/dist/services/redeploy-decision.d.ts +176 -0
  903. package/dist/services/redeploy-decision.d.ts.map +1 -0
  904. package/dist/services/redeploy-decision.js +142 -0
  905. package/dist/services/redeploy-decision.js.map +1 -0
  906. package/dist/services/release.rpc-service.d.ts +159 -0
  907. package/dist/services/release.rpc-service.d.ts.map +1 -0
  908. package/dist/services/release.rpc-service.js +395 -0
  909. package/dist/services/release.rpc-service.js.map +1 -0
  910. package/dist/services/release.service.d.ts +114 -0
  911. package/dist/services/release.service.d.ts.map +1 -0
  912. package/dist/services/release.service.js +208 -0
  913. package/dist/services/release.service.js.map +1 -0
  914. package/dist/services/remote-deployer.service.d.ts +580 -8
  915. package/dist/services/remote-deployer.service.d.ts.map +1 -1
  916. package/dist/services/remote-deployer.service.js +1786 -169
  917. package/dist/services/remote-deployer.service.js.map +1 -1
  918. package/dist/services/remote-ops.service.d.ts +51 -9
  919. package/dist/services/remote-ops.service.d.ts.map +1 -1
  920. package/dist/services/remote-ops.service.js +235 -39
  921. package/dist/services/remote-ops.service.js.map +1 -1
  922. package/dist/services/remote-provisioner.d.ts +124 -0
  923. package/dist/services/remote-provisioner.d.ts.map +1 -0
  924. package/dist/services/remote-provisioner.js +258 -0
  925. package/dist/services/remote-provisioner.js.map +1 -0
  926. package/dist/services/secrets.rpc-service.d.ts +51 -2
  927. package/dist/services/secrets.rpc-service.d.ts.map +1 -1
  928. package/dist/services/secrets.rpc-service.js +81 -2
  929. package/dist/services/secrets.rpc-service.js.map +1 -1
  930. package/dist/services/secrets.service.d.ts +108 -9
  931. package/dist/services/secrets.service.d.ts.map +1 -1
  932. package/dist/services/secrets.service.js +313 -48
  933. package/dist/services/secrets.service.js.map +1 -1
  934. package/dist/services/slave-storage.service.d.ts +107 -0
  935. package/dist/services/slave-storage.service.d.ts.map +1 -1
  936. package/dist/services/slave-storage.service.js +171 -2
  937. package/dist/services/slave-storage.service.js.map +1 -1
  938. package/dist/services/static-carry-forward.d.ts +110 -0
  939. package/dist/services/static-carry-forward.d.ts.map +1 -0
  940. package/dist/services/static-carry-forward.js +433 -0
  941. package/dist/services/static-carry-forward.js.map +1 -0
  942. package/dist/services/sync-policy.d.ts +167 -0
  943. package/dist/services/sync-policy.d.ts.map +1 -0
  944. package/dist/services/sync-policy.js +161 -0
  945. package/dist/services/sync-policy.js.map +1 -0
  946. package/dist/services/sync-summary.d.ts +39 -0
  947. package/dist/services/sync-summary.d.ts.map +1 -0
  948. package/dist/services/sync-summary.js +62 -0
  949. package/dist/services/sync-summary.js.map +1 -0
  950. package/dist/services/sync.rpc-service.d.ts +19 -7
  951. package/dist/services/sync.rpc-service.d.ts.map +1 -1
  952. package/dist/services/sync.rpc-service.js +36 -5
  953. package/dist/services/sync.rpc-service.js.map +1 -1
  954. package/dist/services/sync.service.d.ts +350 -18
  955. package/dist/services/sync.service.d.ts.map +1 -1
  956. package/dist/services/sync.service.js +864 -116
  957. package/dist/services/sync.service.js.map +1 -1
  958. package/dist/services/system-info.rpc-service.d.ts +2 -1
  959. package/dist/services/system-info.rpc-service.d.ts.map +1 -1
  960. package/dist/services/system-info.rpc-service.js.map +1 -1
  961. package/dist/services/system-info.service.d.ts +56 -68
  962. package/dist/services/system-info.service.d.ts.map +1 -1
  963. package/dist/services/system-info.service.js +240 -127
  964. package/dist/services/system-info.service.js.map +1 -1
  965. package/dist/services/telemetry.rpc-service.d.ts.map +1 -1
  966. package/dist/services/telemetry.rpc-service.js +9 -3
  967. package/dist/services/telemetry.rpc-service.js.map +1 -1
  968. package/dist/services/telemetry.service.d.ts +4 -2
  969. package/dist/services/telemetry.service.d.ts.map +1 -1
  970. package/dist/services/telemetry.service.js +4 -2
  971. package/dist/services/telemetry.service.js.map +1 -1
  972. package/dist/services/trace-collector.rpc-service.d.ts +2 -1
  973. package/dist/services/trace-collector.rpc-service.d.ts.map +1 -1
  974. package/dist/services/trace-collector.rpc-service.js.map +1 -1
  975. package/dist/services/trace-collector.service.d.ts +2 -41
  976. package/dist/services/trace-collector.service.d.ts.map +1 -1
  977. package/dist/services/trace-collector.service.js +27 -7
  978. package/dist/services/trace-collector.service.js.map +1 -1
  979. package/dist/services/wait-for-postgres.d.ts +28 -0
  980. package/dist/services/wait-for-postgres.d.ts.map +1 -0
  981. package/dist/services/wait-for-postgres.js +77 -0
  982. package/dist/services/wait-for-postgres.js.map +1 -0
  983. package/dist/shared/alert-expression.d.ts +60 -0
  984. package/dist/shared/alert-expression.d.ts.map +1 -0
  985. package/dist/shared/alert-expression.js +124 -0
  986. package/dist/shared/alert-expression.js.map +1 -0
  987. package/dist/shared/audit-outcome.d.ts +39 -0
  988. package/dist/shared/audit-outcome.d.ts.map +1 -0
  989. package/dist/shared/audit-outcome.js +45 -0
  990. package/dist/shared/audit-outcome.js.map +1 -0
  991. package/dist/shared/build-freshness.d.ts +59 -0
  992. package/dist/shared/build-freshness.d.ts.map +1 -0
  993. package/dist/shared/build-freshness.js +113 -0
  994. package/dist/shared/build-freshness.js.map +1 -0
  995. package/dist/shared/build-stamp.d.ts +26 -0
  996. package/dist/shared/build-stamp.d.ts.map +1 -0
  997. package/dist/shared/build-stamp.js +36 -0
  998. package/dist/shared/build-stamp.js.map +1 -0
  999. package/dist/shared/describe-error.d.ts +22 -0
  1000. package/dist/shared/describe-error.d.ts.map +1 -0
  1001. package/dist/shared/describe-error.js +39 -0
  1002. package/dist/shared/describe-error.js.map +1 -0
  1003. package/dist/shared/dto/alerts.d.ts +101 -0
  1004. package/dist/shared/dto/alerts.d.ts.map +1 -0
  1005. package/dist/shared/dto/alerts.js +9 -0
  1006. package/dist/shared/dto/alerts.js.map +1 -0
  1007. package/dist/shared/dto/auth.d.ts +49 -0
  1008. package/dist/shared/dto/auth.d.ts.map +1 -0
  1009. package/dist/shared/dto/auth.js +14 -0
  1010. package/dist/shared/dto/auth.js.map +1 -0
  1011. package/dist/shared/dto/backups.d.ts +16 -0
  1012. package/dist/shared/dto/backups.d.ts.map +1 -0
  1013. package/dist/shared/dto/backups.js +9 -0
  1014. package/dist/shared/dto/backups.js.map +1 -0
  1015. package/dist/shared/dto/deploy.d.ts +28 -0
  1016. package/dist/shared/dto/deploy.d.ts.map +1 -0
  1017. package/dist/shared/dto/deploy.js +8 -0
  1018. package/dist/shared/dto/deploy.js.map +1 -0
  1019. package/dist/shared/dto/discovery.d.ts +24 -0
  1020. package/dist/shared/dto/discovery.d.ts.map +1 -0
  1021. package/dist/shared/dto/discovery.js +9 -0
  1022. package/dist/shared/dto/discovery.js.map +1 -0
  1023. package/dist/shared/dto/fleet.d.ts +35 -0
  1024. package/dist/shared/dto/fleet.d.ts.map +1 -0
  1025. package/dist/shared/dto/fleet.js +8 -0
  1026. package/dist/shared/dto/fleet.js.map +1 -0
  1027. package/dist/shared/dto/health.d.ts +26 -0
  1028. package/dist/shared/dto/health.d.ts.map +1 -0
  1029. package/dist/shared/dto/health.js +9 -0
  1030. package/dist/shared/dto/health.js.map +1 -0
  1031. package/dist/shared/dto/kubernetes.d.ts +44 -0
  1032. package/dist/shared/dto/kubernetes.d.ts.map +1 -0
  1033. package/dist/shared/dto/kubernetes.js +9 -0
  1034. package/dist/shared/dto/kubernetes.js.map +1 -0
  1035. package/dist/shared/dto/logs.d.ts +77 -0
  1036. package/dist/shared/dto/logs.d.ts.map +1 -0
  1037. package/dist/shared/dto/logs.js +10 -0
  1038. package/dist/shared/dto/logs.js.map +1 -0
  1039. package/dist/shared/dto/metrics.d.ts +65 -0
  1040. package/dist/shared/dto/metrics.d.ts.map +1 -0
  1041. package/dist/shared/dto/metrics.js +17 -0
  1042. package/dist/shared/dto/metrics.js.map +1 -0
  1043. package/dist/shared/dto/nodes.d.ts +347 -0
  1044. package/dist/shared/dto/nodes.d.ts.map +1 -0
  1045. package/dist/shared/dto/nodes.js +8 -0
  1046. package/dist/shared/dto/nodes.js.map +1 -0
  1047. package/dist/shared/dto/pipelines.d.ts +53 -0
  1048. package/dist/shared/dto/pipelines.d.ts.map +1 -0
  1049. package/dist/shared/dto/pipelines.js +9 -0
  1050. package/dist/shared/dto/pipelines.js.map +1 -0
  1051. package/dist/shared/dto/project.d.ts +142 -8
  1052. package/dist/shared/dto/project.d.ts.map +1 -1
  1053. package/dist/shared/dto/services.d.ts +858 -7
  1054. package/dist/shared/dto/services.d.ts.map +1 -1
  1055. package/dist/shared/dto/system-info.d.ts +93 -0
  1056. package/dist/shared/dto/system-info.d.ts.map +1 -0
  1057. package/dist/shared/dto/system-info.js +9 -0
  1058. package/dist/shared/dto/system-info.js.map +1 -0
  1059. package/dist/shared/dto/traces.d.ts +49 -0
  1060. package/dist/shared/dto/traces.d.ts.map +1 -0
  1061. package/dist/shared/dto/traces.js +9 -0
  1062. package/dist/shared/dto/traces.js.map +1 -0
  1063. package/dist/shared/env-config.d.ts +74 -0
  1064. package/dist/shared/env-config.d.ts.map +1 -0
  1065. package/dist/shared/env-config.js +99 -0
  1066. package/dist/shared/env-config.js.map +1 -0
  1067. package/dist/shared/events.d.ts +45 -39
  1068. package/dist/shared/events.d.ts.map +1 -1
  1069. package/dist/shared/events.js +6 -0
  1070. package/dist/shared/events.js.map +1 -1
  1071. package/dist/shared/format.d.ts +1 -1
  1072. package/dist/shared/format.d.ts.map +1 -1
  1073. package/dist/shared/format.js +4 -0
  1074. package/dist/shared/format.js.map +1 -1
  1075. package/dist/shared/local-infra-host.d.ts +28 -0
  1076. package/dist/shared/local-infra-host.d.ts.map +1 -0
  1077. package/dist/shared/local-infra-host.js +28 -0
  1078. package/dist/shared/local-infra-host.js.map +1 -0
  1079. package/dist/shared/node-check.d.ts +37 -0
  1080. package/dist/shared/node-check.d.ts.map +1 -0
  1081. package/dist/shared/node-check.js +41 -0
  1082. package/dist/shared/node-check.js.map +1 -0
  1083. package/dist/shared/paths.d.ts +29 -0
  1084. package/dist/shared/paths.d.ts.map +1 -0
  1085. package/dist/shared/paths.js +54 -0
  1086. package/dist/shared/paths.js.map +1 -0
  1087. package/dist/shared/pnpm.d.ts +29 -0
  1088. package/dist/shared/pnpm.d.ts.map +1 -0
  1089. package/dist/shared/pnpm.js +57 -0
  1090. package/dist/shared/pnpm.js.map +1 -0
  1091. package/dist/shared/private-files.d.ts +29 -0
  1092. package/dist/shared/private-files.d.ts.map +1 -0
  1093. package/dist/shared/private-files.js +109 -0
  1094. package/dist/shared/private-files.js.map +1 -0
  1095. package/dist/shared/redact-env.d.ts +24 -0
  1096. package/dist/shared/redact-env.d.ts.map +1 -0
  1097. package/dist/shared/redact-env.js +36 -0
  1098. package/dist/shared/redact-env.js.map +1 -0
  1099. package/dist/shared/release-reading.d.ts +85 -0
  1100. package/dist/shared/release-reading.d.ts.map +1 -0
  1101. package/dist/shared/release-reading.js +81 -0
  1102. package/dist/shared/release-reading.js.map +1 -0
  1103. package/dist/shared/roles.d.ts +58 -3
  1104. package/dist/shared/roles.d.ts.map +1 -1
  1105. package/dist/shared/roles.js +55 -0
  1106. package/dist/shared/roles.js.map +1 -1
  1107. package/dist/shared/rpcauth.d.ts +13 -0
  1108. package/dist/shared/rpcauth.d.ts.map +1 -0
  1109. package/dist/shared/rpcauth.js +23 -0
  1110. package/dist/shared/rpcauth.js.map +1 -0
  1111. package/dist/shared/shell-escape.d.ts +9 -0
  1112. package/dist/shared/shell-escape.d.ts.map +1 -0
  1113. package/dist/shared/shell-escape.js +11 -0
  1114. package/dist/shared/shell-escape.js.map +1 -0
  1115. package/dist/shared/sync-reading.d.ts +45 -0
  1116. package/dist/shared/sync-reading.d.ts.map +1 -0
  1117. package/dist/shared/sync-reading.js +42 -0
  1118. package/dist/shared/sync-reading.js.map +1 -0
  1119. package/dist/shared/tokens.d.ts +19 -0
  1120. package/dist/shared/tokens.d.ts.map +1 -1
  1121. package/dist/shared/tokens.js +28 -0
  1122. package/dist/shared/tokens.js.map +1 -1
  1123. package/dist/webapp/webapp.service.d.ts +33 -2
  1124. package/dist/webapp/webapp.service.d.ts.map +1 -1
  1125. package/dist/webapp/webapp.service.js +143 -31
  1126. package/dist/webapp/webapp.service.js.map +1 -1
  1127. package/dist/workers/health-monitor-process.d.ts.map +1 -1
  1128. package/dist/workers/health-monitor-process.js +10 -0
  1129. package/dist/workers/health-monitor-process.js.map +1 -1
  1130. package/dist/workers/health-monitor.service.d.ts +45 -3
  1131. package/dist/workers/health-monitor.service.d.ts.map +1 -1
  1132. package/dist/workers/health-monitor.service.js +152 -25
  1133. package/dist/workers/health-monitor.service.js.map +1 -1
  1134. package/dist/workers/system-worker-manager.d.ts +43 -0
  1135. package/dist/workers/system-worker-manager.d.ts.map +1 -1
  1136. package/dist/workers/system-worker-manager.js +150 -3
  1137. package/dist/workers/system-worker-manager.js.map +1 -1
  1138. package/dist/workers/types.d.ts +8 -2
  1139. package/dist/workers/types.d.ts.map +1 -1
  1140. package/package.json +68 -30
  1141. package/webapp/dist/BUILD.json +5 -0
  1142. package/webapp/dist/assets/FormControlLabel-BbSxpQwN.js +1 -0
  1143. package/webapp/dist/assets/Grid-DizFZYue.js +1 -0
  1144. package/webapp/dist/assets/Switch-Bfiqzx8l.js +1 -0
  1145. package/webapp/dist/assets/alerts-DH_RVRS3.js +1 -0
  1146. package/webapp/dist/assets/apps-B96355sz.js +1 -0
  1147. package/webapp/dist/assets/audit-CYGtbi4k.js +1 -0
  1148. package/webapp/dist/assets/client-BmqFExWT.js +1 -0
  1149. package/webapp/dist/assets/constants-veKGFkC6.js +1 -0
  1150. package/webapp/dist/assets/containers-B_TpThln.js +2 -0
  1151. package/webapp/dist/assets/daemon-memory-CCPZ4cV_.js +1 -0
  1152. package/webapp/dist/assets/dashboard-DHf2_65N.js +1 -0
  1153. package/webapp/dist/assets/dashboard-builder-CWY2pjeG.js +1 -0
  1154. package/webapp/dist/assets/deploy-release-dialog-Dp-zpSAd.js +1 -0
  1155. package/webapp/dist/assets/deployments-tJI6O3fK.js +1 -0
  1156. package/webapp/dist/assets/detail-B2zP1Hto.js +8 -0
  1157. package/webapp/dist/assets/detail-CoIrwmsy.js +1 -0
  1158. package/webapp/dist/assets/detail-DWaGaOtL.js +2 -0
  1159. package/webapp/dist/assets/formatters-BqjL73N3.js +1 -0
  1160. package/webapp/dist/assets/index-Dvo0iprx.css +1 -0
  1161. package/webapp/dist/assets/index-vMnY_4jR.js +412 -0
  1162. package/webapp/dist/assets/logs-DTRjOfX8.js +12 -0
  1163. package/webapp/dist/assets/maplibre-gl-DowHoZAx.js +814 -0
  1164. package/webapp/dist/assets/metrics-UCajtdox.js +2 -0
  1165. package/webapp/dist/assets/nodes-DuTVU0f6.js +7 -0
  1166. package/webapp/dist/assets/pipelines-C8BeDcAl.js +4 -0
  1167. package/webapp/dist/assets/projects-Dp_zFfLY.js +1 -0
  1168. package/webapp/dist/assets/react-DlmbjZJV.js +1 -0
  1169. package/webapp/dist/assets/react-apexcharts.esm-C8pn45ml.js +97 -0
  1170. package/webapp/dist/assets/release-bits-BzTKz5fM.js +1 -0
  1171. package/webapp/dist/assets/release-build-panel-DcW9YcXQ.js +1 -0
  1172. package/webapp/dist/assets/releases-B4hcr1bE.js +2 -0
  1173. package/webapp/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  1174. package/webapp/dist/assets/settings-DIteZTbe.js +1 -0
  1175. package/webapp/dist/assets/settled-pair-C7SjZuns.js +1 -0
  1176. package/webapp/dist/assets/sign-in-D4kMBOHp.js +1 -0
  1177. package/webapp/dist/assets/stacks-Brnf-EB7.js +1 -0
  1178. package/webapp/dist/assets/sync-reading-C3PnJ7AX.js +1 -0
  1179. package/webapp/dist/assets/system-info-CFDNVKkU.js +1 -0
  1180. package/webapp/dist/assets/topology-7WeTUsDN.js +12 -0
  1181. package/webapp/dist/assets/topology-DLioOiRN.css +1 -0
  1182. package/webapp/dist/assets/traces-9c3gwCN-.js +1 -0
  1183. package/webapp/dist/assets/use-stack-context-CnA5FTHu.js +1 -0
  1184. package/webapp/dist/index.html +6 -6
  1185. package/dist/commands/dev.d.ts +0 -23
  1186. package/dist/commands/dev.d.ts.map +0 -1
  1187. package/dist/commands/dev.js +0 -76
  1188. package/dist/commands/dev.js.map +0 -1
  1189. package/dist/config/define-app.d.ts +0 -6
  1190. package/dist/config/define-app.d.ts.map +0 -1
  1191. package/dist/config/define-app.js +0 -38
  1192. package/dist/config/define-app.js.map +0 -1
  1193. package/dist/infrastructure/gateway-generator.d.ts +0 -63
  1194. package/dist/infrastructure/gateway-generator.d.ts.map +0 -1
  1195. package/dist/infrastructure/gateway-generator.js +0 -253
  1196. package/dist/infrastructure/gateway-generator.js.map +0 -1
  1197. package/dist/monitoring/health.service.d.ts +0 -14
  1198. package/dist/monitoring/health.service.d.ts.map +0 -1
  1199. package/dist/monitoring/health.service.js +0 -50
  1200. package/dist/monitoring/health.service.js.map +0 -1
  1201. package/dist/monitoring/metrics.service.d.ts +0 -14
  1202. package/dist/monitoring/metrics.service.d.ts.map +0 -1
  1203. package/dist/monitoring/metrics.service.js +0 -40
  1204. package/dist/monitoring/metrics.service.js.map +0 -1
  1205. package/dist/orchestrator/bootstrap-runner.d.ts +0 -15
  1206. package/dist/orchestrator/bootstrap-runner.d.ts.map +0 -1
  1207. package/dist/orchestrator/bootstrap-runner.js +0 -152
  1208. package/dist/orchestrator/bootstrap-runner.js.map +0 -1
  1209. package/dist/orchestrator/topology-proxy.d.ts +0 -65
  1210. package/dist/orchestrator/topology-proxy.d.ts.map +0 -1
  1211. package/dist/orchestrator/topology-proxy.js +0 -209
  1212. package/dist/orchestrator/topology-proxy.js.map +0 -1
  1213. package/dist/orchestrator/topology-relay.d.ts +0 -111
  1214. package/dist/orchestrator/topology-relay.d.ts.map +0 -1
  1215. package/dist/orchestrator/topology-relay.js +0 -255
  1216. package/dist/orchestrator/topology-relay.js.map +0 -1
  1217. package/dist/services/auth-middleware.d.ts +0 -47
  1218. package/dist/services/auth-middleware.d.ts.map +0 -1
  1219. package/dist/services/auth-middleware.js +0 -158
  1220. package/dist/services/auth-middleware.js.map +0 -1
  1221. package/dist/services/environment.rpc-service.d.ts +0 -22
  1222. package/dist/services/environment.rpc-service.d.ts.map +0 -1
  1223. package/dist/services/environment.rpc-service.js +0 -65
  1224. package/dist/services/environment.rpc-service.js.map +0 -1
  1225. package/dist/services/environment.service.d.ts +0 -87
  1226. package/dist/services/environment.service.d.ts.map +0 -1
  1227. package/dist/services/environment.service.js +0 -220
  1228. package/dist/services/environment.service.js.map +0 -1
  1229. package/dist/services/metrics-collector.service.d.ts +0 -103
  1230. package/dist/services/metrics-collector.service.d.ts.map +0 -1
  1231. package/dist/services/metrics-collector.service.js +0 -235
  1232. package/dist/services/metrics-collector.service.js.map +0 -1
  1233. package/dist/services/metrics.rpc-service.d.ts +0 -41
  1234. package/dist/services/metrics.rpc-service.d.ts.map +0 -1
  1235. package/dist/services/metrics.rpc-service.js +0 -90
  1236. package/dist/services/metrics.rpc-service.js.map +0 -1
  1237. package/dist/services/metrics.service.d.ts +0 -72
  1238. package/dist/services/metrics.service.d.ts.map +0 -1
  1239. package/dist/services/metrics.service.js +0 -185
  1240. package/dist/services/metrics.service.js.map +0 -1
  1241. package/dist/supervisor/omnitron-supervisor.d.ts +0 -21
  1242. package/dist/supervisor/omnitron-supervisor.d.ts.map +0 -1
  1243. package/dist/supervisor/omnitron-supervisor.js +0 -43
  1244. package/dist/supervisor/omnitron-supervisor.js.map +0 -1
  1245. package/dist/webapp/static-server.d.ts +0 -24
  1246. package/dist/webapp/static-server.d.ts.map +0 -1
  1247. package/dist/webapp/static-server.js +0 -152
  1248. package/dist/webapp/static-server.js.map +0 -1
  1249. package/webapp/dist/assets/Badge-D2ZBP5iK.js +0 -1
  1250. package/webapp/dist/assets/Card-vO88g06B.js +0 -1
  1251. package/webapp/dist/assets/CardActions-C9h7Q2wW.js +0 -1
  1252. package/webapp/dist/assets/CardContent-BA_Xmmhk.js +0 -1
  1253. package/webapp/dist/assets/CardHeader-ceIs__6x.js +0 -1
  1254. package/webapp/dist/assets/Chip-Ckk0xUhK.js +0 -1
  1255. package/webapp/dist/assets/DialogActions-BhHM7hov.js +0 -1
  1256. package/webapp/dist/assets/DialogContentText-BT2JQRmK.js +0 -1
  1257. package/webapp/dist/assets/FormControlLabel-CcQByVS8.js +0 -1
  1258. package/webapp/dist/assets/Grid-14PETRQW.js +0 -1
  1259. package/webapp/dist/assets/InputAdornment-B8aHIsQ7.js +0 -1
  1260. package/webapp/dist/assets/Skeleton-B74-Ds6O.js +0 -37
  1261. package/webapp/dist/assets/Switch-DvWD2wh4.js +0 -1
  1262. package/webapp/dist/assets/SwitchBase-CDU4nY8O.js +0 -1
  1263. package/webapp/dist/assets/Tab-DcCOYYp5.js +0 -1
  1264. package/webapp/dist/assets/TableContainer-CzTh4mu-.js +0 -1
  1265. package/webapp/dist/assets/TableRow-BySEreZR.js +0 -2
  1266. package/webapp/dist/assets/TextField-BLf-BHz2.js +0 -1
  1267. package/webapp/dist/assets/alerts-DnRbT41C.js +0 -1
  1268. package/webapp/dist/assets/apps-CdR0VH2d.js +0 -1
  1269. package/webapp/dist/assets/chunk-DtcAfS_W.js +0 -1
  1270. package/webapp/dist/assets/client-DU8I_lDc.js +0 -2
  1271. package/webapp/dist/assets/constants-BgnT-Xdy.js +0 -1
  1272. package/webapp/dist/assets/containers-Dw-rRXV1.js +0 -1
  1273. package/webapp/dist/assets/dashboard-CzgB0uZz.js +0 -1
  1274. package/webapp/dist/assets/dashboard-builder-DsA0yg33.js +0 -1
  1275. package/webapp/dist/assets/deployments-DtQAoIcQ.js +0 -1
  1276. package/webapp/dist/assets/detail-BJR-G7JG.js +0 -1
  1277. package/webapp/dist/assets/detail-nlCeSltl.js +0 -8
  1278. package/webapp/dist/assets/formatters-Do41SRmE.js +0 -1
  1279. package/webapp/dist/assets/index-ByQDwp2r.js +0 -566
  1280. package/webapp/dist/assets/index-CHVXOsTS.css +0 -1
  1281. package/webapp/dist/assets/isMuiElement-DwF2naaF.js +0 -1
  1282. package/webapp/dist/assets/logs-Kn8o4H7k.js +0 -11
  1283. package/webapp/dist/assets/metrics-DN8Zu5wT.js +0 -2
  1284. package/webapp/dist/assets/nodes-Bgch7AtN.js +0 -7
  1285. package/webapp/dist/assets/pipelines-Cry3qm-S.js +0 -4
  1286. package/webapp/dist/assets/popper-BJFSj6rc.js +0 -1
  1287. package/webapp/dist/assets/preload-helper-DSXbuxSR.js +0 -1
  1288. package/webapp/dist/assets/projects-Dym2GI1U.js +0 -1
  1289. package/webapp/dist/assets/react-apexcharts.esm-CSGK5Cxn.js +0 -958
  1290. package/webapp/dist/assets/react-apexcharts.esm-DEAWOwx5.js +0 -1003
  1291. package/webapp/dist/assets/settings-va3zTcow.js +0 -1
  1292. package/webapp/dist/assets/sign-in-ik35umbp.js +0 -1
  1293. package/webapp/dist/assets/stacks-CVFOnxbF.js +0 -1
  1294. package/webapp/dist/assets/system-info-npIXAjX2.js +0 -1
  1295. package/webapp/dist/assets/topology-CHpVij2M.css +0 -1
  1296. package/webapp/dist/assets/topology-bDe70vLs.js +0 -12
  1297. package/webapp/dist/assets/traces-DIIcT8DF.js +0 -1
  1298. package/webapp/dist/assets/use-stack-context-DHLwZv1r.js +0 -1
  1299. /package/webapp/dist/assets/{fs-CgZLtHiQ.js → fs-Ccl3sAcp.js} +0 -0
@@ -11,7 +11,13 @@
11
11
  *
12
12
  * Classic mode:
13
13
  * - child_process.fork() with manual exit handling (legacy compat)
14
- * - Real CPU/memory metrics via pidusage
14
+ *
15
+ * Metrics for both modes come from one `ps -o pid=,rss=,%cpu=` call covering
16
+ * every managed pid — supervisor children and pool workers alike. This line
17
+ * used to say "real CPU/memory metrics via pidusage"; `pidusage` is not a
18
+ * dependency of this package and appears nowhere else in the source. The
19
+ * sentence outlived the code it described, in the one area where a stale
20
+ * claim is hardest to catch: metrics that are wrong still look like metrics.
15
21
  *
16
22
  * Architecture:
17
23
  * pm.supervisor({ children: [{ process: bootstrap-process.js, ... }] })
@@ -21,18 +27,59 @@
21
27
  * → Two Netron instances: PM management plane + app HTTP data plane
22
28
  */
23
29
  import fs from 'node:fs';
30
+ import { readDeclaredConfig, withDeclaredConfig } from '../project/declared-config.js';
24
31
  import path from 'node:path';
25
32
  import { fileURLToPath } from 'node:url';
26
- import { PoolStrategy, SupervisionStrategy, } from '@omnitron-dev/titan-pm';
33
+ import { PoolStrategy, SupervisionStrategy, ProcessLifecycleQueue, LifecyclePreempted, computeBackoff, isAlive, discoverManagedProcesses, } from '@omnitron-dev/titan-pm';
27
34
  import { EventEmitter } from '@omnitron-dev/eventemitter';
28
35
  import { AppHandle } from './app-handle.js';
36
+ import { parsePsLine, parsePsBatch } from './ps-metrics.js';
29
37
  import { launchClassic } from './classic-launcher.js';
30
38
  import { resolveStartupOrder, resolveShutdownOrder } from './dependency-resolver.js';
31
39
  import { buildRestartPolicy } from '../supervisor/restart-policy.js';
40
+ /**
41
+ * How long to wait for a pool worker to finish exposing its `@Service`
42
+ * classes before giving up on `topology.expose`. A worker reports ready
43
+ * before its Application starts, and exposure happens during start, so the
44
+ * gap is a real one — measured at ~7s for a worker that opens database
45
+ * connections first. The consumer that needs the service starts right after
46
+ * this returns, so the wait is paid once at boot and buys a feature that
47
+ * otherwise stays off until the next restart.
48
+ */
49
+ /**
50
+ * `childName` is sometimes bare ('http') and sometimes already carries the app
51
+ * ('acme/dev/pricing/http'), depending on which supervisor path raised the
52
+ * event. Prefixing unconditionally produced
53
+ * `acme/dev/pricing/acme/dev/pricing/http` in the log file.
54
+ */
55
+ function qualifyChildName(appName, childName) {
56
+ return childName.startsWith(`${appName}/`) || childName === appName
57
+ ? childName
58
+ : `${appName}/${childName}`;
59
+ }
60
+ const TOPOLOGY_EXPOSE_TIMEOUT_MS = 30_000;
61
+ const TOPOLOGY_EXPOSE_POLL_MS = 250;
32
62
  import { ServiceRouter } from './service-router.js';
33
63
  import { loadBootstrapConfig, clearCacheFor } from './bootstrap-loader.js';
64
+ /** Levels a pino logger accepts. A child is spawned with nothing else. */
65
+ const PINO_LEVELS = new Set(['fatal', 'error', 'warn', 'info', 'debug', 'trace', 'silent']);
66
+ /**
67
+ * Process-manager states in which a record no longer describes a live process.
68
+ *
69
+ * Read by `managerOwnedPids`, where the distinction decides whether a pid is
70
+ * protected from the janitor. Written as strings rather than imported from
71
+ * `ProcessStatus` because that export is a const object whose values are
72
+ * these same literals; the set has to hold values, not keys, and a typo here
73
+ * would silently protect a dead pid — so it is pinned by a test.
74
+ */
75
+ const TERMINAL_PROCESS_STATUSES = new Set(['stopped', 'failed', 'crashed']);
34
76
  import { BuildService } from './build-service.js';
77
+ import { execFile } from 'node:child_process';
35
78
  import { ProcessJanitor } from './process-janitor.js';
79
+ import { addressInUse, holderCommand, parseHolders, explainConflict } from './port-conflict.js';
80
+ import { collectOwnedPids } from './owned-pids.js';
81
+ import { combineProcessHealth, notRunningHealth, poolHealth } from './app-health.js';
82
+ import { combineProcessTraffic } from './app-traffic.js';
36
83
  import { CLI_VERSION } from '../config/defaults.js';
37
84
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
38
85
  // Resolve bootstrap-process path — .ts in dev (tsx), .js when compiled
@@ -63,15 +110,77 @@ function mapStrategy(strategy) {
63
110
  * stack mode + bare-name RPC paths can be deduplicated.
64
111
  *
65
112
  * Examples:
66
- * effectiveAppName('omni/dev/paysys') === 'paysys'
67
- * effectiveAppName('paysys') === 'paysys'
68
- * effectiveAppName('a/b/c/paysys') === 'paysys' (last segment)
113
+ * effectiveAppName('omni/dev/payments') === 'payments'
114
+ * effectiveAppName('payments') === 'payments'
115
+ * effectiveAppName('a/b/c/payments') === 'payments' (last segment)
69
116
  */
70
- function effectiveAppName(key) {
117
+ export function effectiveAppName(key) {
71
118
  const idx = key.lastIndexOf('/');
72
119
  return idx >= 0 ? key.slice(idx + 1) : key;
73
120
  }
121
+ /**
122
+ * Auto-promote a bare `entry.name` to canonical `${project}/${stack}/${name}`
123
+ * form when the env carries OMNITRON_PROJECT + OMNITRON_STACK. The stack-mode
124
+ * boot path (ProjectService.startStack) already prefixes the name explicitly;
125
+ * this helper closes every other entry point that could otherwise register a
126
+ * bare-name handle which then becomes invisible to the stack-prefix-based
127
+ * UI lookup (ProjectService.toStackInfo filters on `${project}/${stack}/`).
128
+ *
129
+ * Idempotent: if the entry already looks namespaced (or env lacks the keys),
130
+ * the input is returned unchanged. An entry qualified under a DIFFERENT
131
+ * project/stack is preserved too — that's operator intent (cross-project
132
+ * administrative run), not a bug.
133
+ */
134
+ export function ensureNamespacedEntry(entry) {
135
+ const project = entry.env?.['OMNITRON_PROJECT'];
136
+ const stack = entry.env?.['OMNITRON_STACK'];
137
+ if (!project || !stack)
138
+ return entry;
139
+ const canonicalPrefix = `${project}/${stack}/`;
140
+ if (entry.name.startsWith(canonicalPrefix))
141
+ return entry;
142
+ if (entry.name.includes('/'))
143
+ return entry;
144
+ return { ...entry, name: `${canonicalPrefix}${entry.name}` };
145
+ }
74
146
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
147
+ /**
148
+ * Startup budget for a child process that declares none of its own.
149
+ *
150
+ * There were TWO of these and they disagreed — `?? 60_000` on the pooled path
151
+ * and `?? 30_000` on the single-process path — and, worse, the pooled one did
152
+ * not consult `entry.startupTimeout` at all. So an operator who set
153
+ * `startupTimeout: 120_000` on an app had it honoured for the app and
154
+ * SILENTLY IGNORED for its child processes, which kept 60 seconds and could
155
+ * not be given more by any setting.
156
+ *
157
+ * Measured on this host: an app whose own successful boot takes 120.6 seconds
158
+ * carried a 120 s budget, and its `deposit-worker` child — a deposit scanner
159
+ * — stayed `stopped` while the app itself came up. Two copies of one decision,
160
+ * one of which discarded the only knob an operator has.
161
+ *
162
+ * 60 s rather than 30 s, for the reason the project's own config already
163
+ * writes down: an over-long ceiling costs a slower failure report, a short one
164
+ * costs the whole stack.
165
+ */
166
+ const DEFAULT_CHILD_STARTUP_TIMEOUT = 60_000;
167
+ /**
168
+ * Deadline for an RPC the daemon makes to a child it supervises.
169
+ *
170
+ * A call across this connection is WORK — aggregate five minutes of candles,
171
+ * answer a health probe that touches a pool, hand back metrics — not a wire
172
+ * request, and netron's default (5000 ms) is sized for the latter. The pool
173
+ * path stated this number and the single-child path stated nothing, so a
174
+ * client built for a single child fell back to that default: measured, 94
175
+ * timeouts «after 5000ms» in the daemon log, every one on
176
+ * `callExposedService`, which lives on exactly those single children.
177
+ *
178
+ * It belongs here rather than in titan-pm because the orchestrator is what
179
+ * knows the nature of the work; the library's own fallback is 30 s
180
+ * (`process-pool.ts:212`) and applies when a caller states nothing — which,
181
+ * after this, no caller here does.
182
+ */
183
+ const CHILD_REQUEST_TIMEOUT = 120_000;
75
184
  export class OrchestratorService extends EventEmitter {
76
185
  logger;
77
186
  pm;
@@ -88,6 +197,30 @@ export class OrchestratorService extends EventEmitter {
88
197
  * for the same name isn't permanently locked out.
89
198
  */
90
199
  startingApps = new Map();
200
+ /**
201
+ * Per-app async serial-execution queue covering start / stop / restart
202
+ * / crash-restart-timer (P0-E + P0-I + P2-B in the audit). Pre-this-
203
+ * queue each path had its own coalescer:
204
+ * - startingApps: same-name start coalescing only — `stopApp` did
205
+ * NOT consult it, so stop racing in-flight start left an orphan
206
+ * supervisor when stop arrived before start wrote childProcess.
207
+ * - restartCoalescer: same-name restart coalescing; independent of
208
+ * stop+start.
209
+ * - handleClassicCrash setTimeout: a raw timer that could fire
210
+ * during the `await stopChild(...)` window and spawn a zombie
211
+ * against an app the operator just stopped.
212
+ * The queue collapses all four into one primitive ordered by
213
+ * priority (stop > restart > start > crash-restart). The same-key
214
+ * mutex means stop ALWAYS observes a completed start, and an
215
+ * explicit stop ALWAYS preempts a pending crash-restart timer.
216
+ */
217
+ lifecycleQueue = new ProcessLifecycleQueue();
218
+ /**
219
+ * Pending post-escalation cool-down timers, keyed by app name.
220
+ * Held here rather than inside a lifecycle-queue slot so that a
221
+ * crashed app still answers operator commands while it waits.
222
+ */
223
+ coolDownTimers = new Map();
91
224
  config = null;
92
225
  cwd;
93
226
  metricsTimer = null;
@@ -96,6 +229,56 @@ export class OrchestratorService extends EventEmitter {
96
229
  devMode = false;
97
230
  /** esbuild bundling pipeline — pre-bundles TS entry points for child processes */
98
231
  buildService = null;
232
+ watchDisabled = false;
233
+ watchSelection = null;
234
+ bootstrapSources = new Map();
235
+ allowsWatch(entry) {
236
+ return this.devMode && !this.watchDisabled && entry.watch !== false &&
237
+ (!this.watchSelection || this.watchSelection.has(entry.name));
238
+ }
239
+ /** One policy for both fs.watch and esbuild; disabling does not stop apps. */
240
+ async setWatchMode(enabled, apps) {
241
+ this.watchDisabled = !enabled;
242
+ this.watchSelection = apps ? new Set(apps.map((name) => this.resolveAppName(name) ?? name)) : null;
243
+ for (const [name, state] of this.restartCoalescer) {
244
+ if (!enabled || !this.handles.get(name) || !this.allowsWatch(this.handles.get(name).entry))
245
+ state.watchSerial = 0;
246
+ }
247
+ await Promise.all([...this.bootstrapSources.keys()].map((name) => this.syncBootstrapWatch(name)));
248
+ }
249
+ getBuildWatchedApps() {
250
+ return [...this.bootstrapSources].filter(([name]) => this.buildService?.isWatching(name))
251
+ .map(([name, source]) => ({ name, directory: path.dirname(source.path) }));
252
+ }
253
+ async syncBootstrapWatch(name) {
254
+ const source = this.bootstrapSources.get(name);
255
+ const entry = this.handles.get(name)?.entry;
256
+ if (!this.buildService || !source)
257
+ return;
258
+ if (!entry || !this.allowsWatch(entry) || !entry.bootstrap ||
259
+ path.resolve(entry.cwd ?? this.cwd, entry.bootstrap) !== source.path) {
260
+ const state = this.restartCoalescer.get(name);
261
+ if (state)
262
+ state.watchSerial = 0;
263
+ await this.buildService.unwatchApp(name);
264
+ return;
265
+ }
266
+ if (this.buildService.isWatching(name))
267
+ return;
268
+ await this.buildService.watchApp(name, source.path, source.definition, () => {
269
+ void this.restartAfterChange(name).catch((error) => {
270
+ this.logger.error({ app: name, error: error.message }, 'Restart after source change failed');
271
+ });
272
+ });
273
+ }
274
+ /** Automatic triggers remain revocable while queued; manual restart is independent. */
275
+ async restartAfterChange(name) {
276
+ const canonical = this.resolveAppName(name) ?? name;
277
+ const handle = this.handles.get(canonical);
278
+ if (!handle || !this.allowsWatch(handle.entry))
279
+ return handle;
280
+ return this.restartAppCoalesced(canonical, true, 'watch');
281
+ }
99
282
  /** Cached build results per app name */
100
283
  buildResults = new Map();
101
284
  /**
@@ -129,10 +312,23 @@ export class OrchestratorService extends EventEmitter {
129
312
  */
130
313
  static POST_ONLINE_COOLDOWN_MS = 3_000;
131
314
  /**
132
- * Orphan-process reaper. Lazily created on first `startAll` so unit
133
- * tests that hit `startApp` directly don't get a periodic timer.
315
+ * Orphan-process reaper. Lazily created on first reconcile so unit
316
+ * tests that hit `startApp` directly don't get a periodic timer
317
+ * until the first real reconcile runs (which they bypass by
318
+ * stubbing `bootReconciled = true`).
134
319
  */
135
320
  janitor = null;
321
+ /**
322
+ * One-shot guard for cold-start reconciliation. T#63 — every code
323
+ * path that lands an app on this orchestrator goes through
324
+ * `ensureBootReconciled()`, regardless of whether the entry-point
325
+ * is `startAll` (daemon-config) or `startApp` (project/stack
326
+ * mode). Pre-T#63 the cold-start sweep only fired inside
327
+ * `startAll`, so project-mode boots silently inherited orphans
328
+ * from the previous daemon generation.
329
+ */
330
+ bootReconciled = false;
331
+ bootReconcilePromise = null;
136
332
  /** Daemon's Netron instance for native service routing */
137
333
  daemonNetron = null;
138
334
  /** Daemon Unix socket path for child-to-daemon Netron connections */
@@ -220,13 +416,117 @@ export class OrchestratorService extends EventEmitter {
220
416
  // ============================================================================
221
417
  // Lifecycle
222
418
  // ============================================================================
419
+ /**
420
+ * Refresh the orchestrator's view of the ecosystem config. Called
421
+ * from `daemon.reloadConfig` so a subsequent `restartApp` reads the
422
+ * new entry definition (mode, env, dependsOn, restart policy)
423
+ * instead of the stale copy captured at first `startAll`. Pre-fix
424
+ * the two refs diverged silently after every reload — operators
425
+ * editing ecosystem.config.ts and running `omnitron reload`
426
+ * believed the daemon picked up the changes; in fact only daemon's
427
+ * own RPC service did, and individual app restarts kept running
428
+ * against the pre-reload definition until the daemon itself
429
+ * restarted. P0-G in the audit.
430
+ */
431
+ setConfig(config) {
432
+ this.config = config;
433
+ // Metrics polling is armed here rather than in `startAll` alone.
434
+ //
435
+ // It used to run only from `startAll`, which is the `omnitron start`
436
+ // path. Apps brought up any other way — `stack start`, a single
437
+ // `omnitron start <app>`, or the daemon resuming a stack at boot — were
438
+ // never sampled, so `omnitron list`, `status` and the console's app
439
+ // cards reported cpu 0 / memory 0 for processes that were serving
440
+ // traffic. Observed live: six apps online, every one showing 0 while
441
+ // `ps` reported ~105 MB RSS apiece.
442
+ //
443
+ // `setConfig` is the one point every path passes through, and the call
444
+ // is idempotent — an existing timer is cleared first.
445
+ this.ensureMetricsPolling();
446
+ }
447
+ /**
448
+ * Start metrics sampling if it is not already running.
449
+ *
450
+ * Called from every path that can produce a managed process, so no app can
451
+ * end up unsampled because of how it was started.
452
+ */
453
+ ensureMetricsPolling() {
454
+ if (this.metricsTimer)
455
+ return;
456
+ this.startMetricsPolling(this.config?.monitoring?.metrics?.interval ?? 5_000);
457
+ }
458
+ /**
459
+ * One-shot cold-start reconciliation: discover pre-existing managed
460
+ * processes, reap fork-workers left over from a previous daemon
461
+ * generation, and arm the periodic janitor sweep. Idempotent —
462
+ * concurrent callers join the in-flight promise; subsequent calls
463
+ * after success are no-ops.
464
+ *
465
+ * Called from BOTH `startAll` (top-level daemon-config boot) and
466
+ * `startApp` (project/stack mode boot), so every code path that
467
+ * lands an app on this orchestrator runs the cleanup. Pre-T#63 the
468
+ * cold-start sweep was gated on `startAll` and never ran in
469
+ * project mode, so an orphan from a previous daemon survived
470
+ * silently and held the listen port hostage from its successor.
471
+ */
472
+ async ensureBootReconciled(config) {
473
+ if (this.bootReconciled)
474
+ return;
475
+ if (this.bootReconcilePromise) {
476
+ await this.bootReconcilePromise;
477
+ return;
478
+ }
479
+ this.bootReconcilePromise = (async () => {
480
+ // Discovery: surface OMNITRON_MANAGED-tagged processes that match
481
+ // a configured app so the operator can see what's about to be
482
+ // reaped (or — once the ChildContract handshake lands — adopted).
483
+ // P2-A in the audit.
484
+ try {
485
+ const discovered = discoverManagedProcesses();
486
+ const adopted = [];
487
+ const knownApps = new Set(config?.apps.map((a) => a.name) ?? []);
488
+ for (const proc of discovered) {
489
+ if (proc.ppid === process.pid)
490
+ continue; // our own freshly-spawned child
491
+ const fqn = proc.fullyQualifiedName;
492
+ const bare = proc.appName;
493
+ if (knownApps.has(fqn) || knownApps.has(bare)) {
494
+ adopted.push(`${fqn}#${proc.pid}`);
495
+ }
496
+ }
497
+ if (adopted.length > 0) {
498
+ this.logger.warn({ discovered: adopted.length, apps: adopted }, 'Found pre-existing managed processes — they will be reaped by the janitor in 60s unless adopted');
499
+ }
500
+ }
501
+ catch (err) {
502
+ this.logger.debug({ err: err.message }, 'Process discovery failed — proceeding without reconcile');
503
+ }
504
+ // Reap any fork-workers left over from a previous daemon BEFORE
505
+ // launching apps. Without this, a hard-killed daemon's children
506
+ // are still alive (now reparented to init); they hold DB
507
+ // connections and listen ports that the new apps will fight
508
+ // for. The reap is bounded by `gracefulMs` (default 3s).
509
+ await this.ensureJanitor().coldStartSweep();
510
+ // Arm the periodic sweep so runtime leaks from partially-failed
511
+ // restart cycles (esbuild-rebuild → spawn-fail → orphan) get
512
+ // collected on every interval.
513
+ this.ensureJanitor().start();
514
+ })();
515
+ try {
516
+ await this.bootReconcilePromise;
517
+ this.bootReconciled = true;
518
+ }
519
+ finally {
520
+ this.bootReconcilePromise = null;
521
+ }
522
+ }
223
523
  async startAll(config) {
224
524
  this.config = config;
225
- // Reap any fork-workers left over from a previous daemon BEFORE
226
- // launching apps. Without this, a hard-killed daemon's children
227
- // are still alive (now adopted by init); they hold DB connections
228
- // and sockets that the new apps will fight for.
229
- await this.ensureJanitor().coldStartSweep();
525
+ // Cold-start orphan reap + periodic janitor arming. Idempotent —
526
+ // safe to call on every boot path. Project-mode flows that call
527
+ // `startApp` directly trigger the same one-shot reconcile, so
528
+ // every boot path lands in the same post-condition. (T#63)
529
+ await this.ensureBootReconciled(config);
230
530
  // Dependency-aware skip: if app A fails to start, every app that
231
531
  // declares A in `dependsOn` (transitively) is skipped instead of
232
532
  // attempted. Without this, a downstream app would launch, fail to
@@ -255,10 +555,8 @@ export class OrchestratorService extends EventEmitter {
255
555
  if (failed.size > 0 || blocked.size > 0) {
256
556
  this.logger.warn({ failed: [...failed], blocked: [...blocked] }, 'Startup completed with failures — some apps did not launch');
257
557
  }
258
- // Periodic sweep handles runtime leaks from partially-failed
259
- // restart cycles (esbuild-rebuild → spawn-fail → orphan).
260
- this.ensureJanitor().start();
261
- this.startMetricsPolling(config.monitoring.metrics.interval);
558
+ // Periodic sweep was armed by `ensureBootReconciled` above, and metrics
559
+ // polling by `setConfig` — no second call needed for either.
262
560
  this.persistState();
263
561
  }
264
562
  /**
@@ -276,44 +574,143 @@ export class OrchestratorService extends EventEmitter {
276
574
  return this.janitor;
277
575
  }
278
576
  /**
279
- * Walk every live handle's supervisor children and collect the OS
280
- * pids the orchestrator currently considers ours. Cheap — called
281
- * every janitor sweep.
577
+ * Every OS pid the orchestrator currently considers ours. Cheap — called
578
+ * on every janitor sweep, and the janitor kills whatever this set omits.
579
+ *
580
+ * The rule itself lives in `owned-pids.ts`, where it can be tested: the
581
+ * janitor's own tests supply this set as a fixture, so they could never
582
+ * catch it being incomplete — and it was, by a whole class of process.
282
583
  */
584
+ /**
585
+ * Who is holding the port an app could not bind, in one sentence.
586
+ *
587
+ * Called where a start or restart failed, because that is the moment the
588
+ * answer is worth having and the moment nobody has it: the error names the
589
+ * address and never the holder. Measured on this daemon — four
590
+ * `EADDRINUSE` failures on 3005 in thirty-one seconds, `omnitron list`
591
+ * reporting the app crashed with no pid, and the port held the whole time
592
+ * by a child of this very daemon that nothing in its records claimed.
593
+ *
594
+ * Returns null when the failure was not a port clash, and says so out loud
595
+ * when it could not read the machine — a reading that failed and a port
596
+ * nobody holds are different findings.
597
+ */
598
+ async explainPortConflict(message) {
599
+ const address = addressInUse(message);
600
+ if (!address)
601
+ return null;
602
+ const { file, args } = holderCommand(address.port, process.platform);
603
+ let output;
604
+ try {
605
+ output = await new Promise((resolve, reject) => {
606
+ execFile(file, args, { encoding: 'utf8', timeout: 5_000 }, (err, stdout) => {
607
+ // `ss` and `lsof` both exit non-zero when nothing matches, which is
608
+ // an answer rather than a failure; the empty stdout carries it.
609
+ if (err && !stdout)
610
+ reject(err);
611
+ else
612
+ resolve(stdout);
613
+ });
614
+ });
615
+ }
616
+ catch (err) {
617
+ return `could not read who holds port ${address.port}: ${file} ${err.message}`;
618
+ }
619
+ const holders = parseHolders(output, process.platform);
620
+ // One `ps` for the parents, before deciding: the decision is pure and
621
+ // takes the answers, so that it can be tested without a machine.
622
+ const parents = new Map();
623
+ for (const holder of holders) {
624
+ const ppid = await this.parentOf(holder.pid);
625
+ if (ppid !== undefined)
626
+ parents.set(holder.pid, ppid);
627
+ }
628
+ const verdict = explainConflict({
629
+ port: address.port,
630
+ holders,
631
+ parentOf: (pid) => parents.get(pid),
632
+ daemonPid: process.pid,
633
+ owned: this.collectOwnedPids(),
634
+ });
635
+ return verdict.because;
636
+ }
637
+ /** The parent of one process, or undefined when it cannot be read. */
638
+ async parentOf(pid) {
639
+ try {
640
+ const out = await new Promise((resolve, reject) => {
641
+ execFile('ps', ['-o', 'ppid=', '-p', String(pid)], { encoding: 'utf8', timeout: 5_000 }, (err, stdout) => {
642
+ if (err && !stdout)
643
+ reject(err);
644
+ else
645
+ resolve(stdout);
646
+ });
647
+ });
648
+ const parsed = Number(out.trim());
649
+ return Number.isInteger(parsed) ? parsed : undefined;
650
+ }
651
+ catch {
652
+ // A process that exited between the two readings has no parent to
653
+ // report, and that is not an error worth a line of its own — the
654
+ // verdict already says the holder was gone.
655
+ return undefined;
656
+ }
657
+ }
283
658
  collectOwnedPids() {
284
- const owned = new Set();
285
- for (const handle of this.handles.values()) {
286
- const supervisor = handle.supervisor;
287
- if (!supervisor)
659
+ return collectOwnedPids(this.handles.values(), (workerId) => this.pm.getWorkerHandle(workerId)?.pid, this.managerOwnedPids());
660
+ }
661
+ /**
662
+ * Every pid the process manager currently vouches for.
663
+ *
664
+ * Two windows, and a process is only safe if both are asked. `getForkedPids`
665
+ * holds a claim from `fork()` until `spawn()` resolves and releases it —
666
+ * after that its own docstring says "the WorkerHandle registry is the
667
+ * authority". `listProcesses()` IS that registry, and it is the only source
668
+ * that answers for a process the orchestrator did not start: the daemon's
669
+ * system workers go through this same manager but belong to no app handle,
670
+ * so walking `this.handles` can never see them.
671
+ *
672
+ * Terminal records are excluded deliberately. They keep the pid they had,
673
+ * the OS reuses pids, and a stale one here would vouch for exactly the
674
+ * leaked worker the janitor exists to reap.
675
+ */
676
+ managerOwnedPids() {
677
+ const pids = [...(this.pm.getForkedPids?.() ?? [])];
678
+ for (const info of this.pm.listProcesses?.() ?? []) {
679
+ if (typeof info.pid !== 'number')
288
680
  continue;
289
- for (const childName of supervisor.getChildNames()) {
290
- const processId = supervisor.getChildProcessId(childName);
291
- if (!processId)
292
- continue;
293
- const workerHandle = this.pm.getWorkerHandle(processId);
294
- const pid = workerHandle?.pid;
295
- if (typeof pid === 'number')
296
- owned.add(pid);
297
- }
681
+ if (TERMINAL_PROCESS_STATUSES.has(info.status))
682
+ continue;
683
+ pids.push(info.pid);
298
684
  }
299
- return owned;
685
+ return pids;
300
686
  }
301
687
  async startApp(entry, config) {
302
- // T#57: coalesce concurrent calls for the same app name. Without
303
- // this, two near-simultaneous `startApp({ name: 'main' })` calls
304
- // both see `this.handles.get('main')` as undefined-or-not-online
305
- // (the first one's handle is in status='starting'), both fall
306
- // through to `launchBootstrapMode`, and we spawn two parallel
307
- // worker pools competing for the same port. Promise coalescing
308
- // — return the in-flight promise to the second caller — gives
309
- // both callers the same handle and ensures only one launch
310
- // actually runs.
688
+ // T#63: run the one-shot cold-start reconcile (orphan reap +
689
+ // janitor arming) before ANY app spawns. Project-mode boots flow
690
+ // through `projectService.startStack → orchestrator.startApp`
691
+ // and historically bypassed the reap entirely — a previous
692
+ // daemon's children (now reparented to init) kept their HTTP
693
+ // listen ports and the fresh child failed to bind silently.
694
+ // No-op after first call thanks to `bootReconciled`.
695
+ await this.ensureBootReconciled(config);
696
+ // Defensive auto-namespacing — promotes a bare entry.name to the
697
+ // canonical `${project}/${stack}/${name}` form when the env carries
698
+ // OMNITRON_PROJECT + OMNITRON_STACK. Catches every code path that
699
+ // bypasses ProjectService.startStack:
700
+ // - direct CLI `omnitron start <name>` (no stack context)
701
+ // - legacy ecosystem configs without per-app namespace
702
+ // - daemon startAll() flows over a not-yet-namespaced config
703
+ entry = ensureNamespacedEntry(entry);
311
704
  const inflight = this.startingApps.get(entry.name);
312
705
  if (inflight) {
313
706
  this.logger.debug({ app: entry.name }, 'Coalescing concurrent startApp into in-flight launch (T#57)');
314
707
  return inflight;
315
708
  }
316
- const promise = this.startAppInternal(entry, config).finally(() => {
709
+ // Serialise against stop/restart for the same app via the
710
+ // lifecycle queue. A stopApp arriving while this start is queued
711
+ // (or in-flight) now waits its turn instead of racing through
712
+ // and leaving an orphan supervisor.
713
+ const promise = this.lifecycleQueue.enqueue(entry.name, 'start', () => this.startAppInternal(entry, config)).finally(() => {
317
714
  this.startingApps.delete(entry.name);
318
715
  });
319
716
  this.startingApps.set(entry.name, promise);
@@ -339,17 +736,44 @@ export class OrchestratorService extends EventEmitter {
339
736
  // - exact key already exists, online → return it (idempotent)
340
737
  // - exact key already exists, not online → fall through and re-launch
341
738
  // under the same key
342
- // - DIFFERENT key with the same effective short name → reject:
343
- // operator should restart by canonical name, not register an
344
- // alias.
739
+ // - different fully qualified keys are distinct apps and coexist
740
+ // - bare/qualified keys with the same effective short name:
741
+ // - new is MORE specific (namespaced) and existing is bare → stop
742
+ // the bare one and re-launch under the namespaced key. Without
743
+ // this, a boot-race where bare-name auto-start beats the
744
+ // project/stack flow strands the app under the wrong key and
745
+ // webapp/CLI lookups by namespaced key see it as stopped.
746
+ // - new is LESS-or-equally specific → refuse: operator should
747
+ // restart by canonical name, not register an alias.
345
748
  const effective = effectiveAppName(entry.name);
749
+ const newIsNamespaced = entry.name.includes('/');
750
+ if (!newIsNamespaced && [...this.handles.keys()].filter(key => key.includes('/') && effectiveAppName(key) === effective).length > 1) {
751
+ throw new Error(`Ambiguous app name '${entry.name}'; use its full project/stack/app identity`);
752
+ }
346
753
  for (const [key, h] of this.handles) {
347
754
  if (key === entry.name)
348
755
  continue;
349
756
  if (effectiveAppName(key) !== effective)
350
757
  continue;
351
- // Same effective app under a different key.
758
+ const existingIsNamespaced = key.includes('/');
759
+ // Qualified identities are separate deployments, even when their basename matches.
760
+ if (newIsNamespaced && existingIsNamespaced)
761
+ continue;
762
+ // A bare alias may still refer to the one qualified deployment.
352
763
  if (h.status === 'online') {
764
+ if (newIsNamespaced && !existingIsNamespaced) {
765
+ this.logger.info({ stale: key, replacing_with: entry.name, effective }, 'Stopping bare-name handle to re-register under namespaced key');
766
+ try {
767
+ await this.stopApp(key);
768
+ }
769
+ catch (err) {
770
+ this.logger.warn({ stale: key, error: err.message }, 'stopApp during dedup failed — clearing handle anyway');
771
+ }
772
+ this.handles.delete(key);
773
+ if (key !== entry.name)
774
+ this.metricsBridge?.evictApp(key);
775
+ continue;
776
+ }
353
777
  this.logger.warn({ existing: key, attempted: entry.name, effective }, 'App already running under a canonical name — refusing duplicate registration');
354
778
  return h;
355
779
  }
@@ -357,6 +781,12 @@ export class OrchestratorService extends EventEmitter {
357
781
  // failed previous launch doesn't permanently block the canonical
358
782
  // form.
359
783
  this.logger.info({ stale: key, replacing_with: entry.name, effective }, 'Replacing stale duplicate handle');
784
+ // The handle is errored or stopped, so `stopApp` never ran for it and
785
+ // its services are still registered on the daemon's Netron. Dropping the
786
+ // handle here is the last moment anything knows they exist — or that its
787
+ // processes still run: an errored app's other children and its pools.
788
+ await this.releaseExposedServices(h);
789
+ await this.tearDownProcesses(h);
360
790
  this.handles.delete(key);
361
791
  // Drop any metrics that were collected under the stale alias so it
362
792
  // doesn't haunt future snapshots as a ghost offline app.
@@ -368,9 +798,40 @@ export class OrchestratorService extends EventEmitter {
368
798
  this.logger.warn({ app: entry.name }, 'App already running');
369
799
  return existing;
370
800
  }
801
+ // A handle that is not online may still run processes. A crash restart and
802
+ // a cool-down recovery only flip its status before coming here; one child
803
+ // crashing leaves its siblings — the server among them — up; and its pools
804
+ // were never any supervisor's to stop. Stopped before the new handle
805
+ // starts, or the new one finds its own predecessor on its port: on
806
+ // sample/test (2026-09-25) payments's crash restarts failed five times on
807
+ // `EADDRINUSE :3004`, «held by … a child this daemon still accounts for».
808
+ if (existing) {
809
+ await this.releaseExposedServices(existing);
810
+ await this.tearDownProcesses(existing);
811
+ }
371
812
  const mode = entry.bootstrap ? 'bootstrap' : 'classic';
372
813
  const handle = new AppHandle(entry, mode);
814
+ // The restart budget belongs to the APP, not to this handle instance.
815
+ // Every start replaces the handle, so a counter that starts at zero here
816
+ // can never reach `maxRestarts` — which made the budget, the escalation
817
+ // and the cool-down unreachable even once the restart itself worked. The
818
+ // window is enforced by the policy, not by forgetting.
819
+ if (existing)
820
+ handle.restarts = existing.restarts;
373
821
  this.handles.set(entry.name, handle);
822
+ // Arm metrics sampling here — the one place an app enters the registry.
823
+ //
824
+ // It used to be armed only from `startAll` (the `omnitron start` path).
825
+ // Anything else — `stack start`, a single `omnitron start <app>`, the
826
+ // daemon resuming a stack at boot — was never sampled, so `list`,
827
+ // `status` and the console's app cards reported cpu 0 / memory 0 for
828
+ // processes serving traffic. Observed: six apps online, every one at
829
+ // zero, while `ps` showed ~105 MB RSS apiece.
830
+ //
831
+ // `setConfig` looked like the right seam but is only called on config
832
+ // RELOAD, so it never fired on a normal start. This is the seam that
833
+ // actually covers every path. Idempotent: re-arming replaces the timer.
834
+ this.ensureMetricsPolling();
374
835
  this.logger.info({ app: entry.name, mode }, 'Starting app');
375
836
  try {
376
837
  if (mode === 'classic') {
@@ -379,47 +840,115 @@ export class OrchestratorService extends EventEmitter {
379
840
  else {
380
841
  await this.launchBootstrapMode(entry, handle, cfg);
381
842
  }
382
- this.logger.info({ app: entry.name, pid: handle.pid, mode }, 'App started');
843
+ // «App started» only for an app that is. It was logged in the same
844
+ // millisecond as a child's «Process failed to start», for an app whose
845
+ // processes never all came up.
846
+ if (handle.status === 'online') {
847
+ this.logger.info({ app: entry.name, pid: handle.pid, mode }, 'App started');
848
+ }
849
+ else {
850
+ this.logger.error({ app: entry.name, status: handle.status, mode }, 'App did not come up — not every declared process started');
851
+ }
383
852
  this.persistState();
384
853
  return handle;
385
854
  }
386
855
  catch (err) {
856
+ // `errored` rather than `crashed` is the right REPORT — the app never
857
+ // ran. It was also, until now, the end of the story: `'errored'` is read
858
+ // by no code in this repository outside formatters, `status` and a
859
+ // metrics label, so a start that failed was never attempted again. One
860
+ // try, then errored until a human noticed.
861
+ //
862
+ // That asymmetry is backwards. A failure to start is MORE likely to be
863
+ // transient than a crash after a successful boot — a dependency still
864
+ // coming up, a disk still cold, a machine briefly loaded. Six bootstrap
865
+ // apps here timed out at `config:loading` under a load average of 149
866
+ // and stayed down after the load passed, because nothing asked again.
867
+ //
868
+ // The counter is bumped by hand because `markErrored()` deliberately
869
+ // does not (an app that never ran has not "restarted"), and without it
870
+ // the budget below can never trip and the retry would be unbounded.
387
871
  handle.markErrored();
872
+ handle.restarts++;
388
873
  this.persistState();
874
+ this.scheduleRecovery(entry, handle, cfg);
389
875
  throw err;
390
876
  }
391
877
  }
392
- async stopApp(name, force = false, timeout = 10_000) {
393
- const canonical = this.resolveAppName(name) ?? name;
394
- const handle = this.handles.get(canonical);
395
- if (!handle || handle.status === 'stopped')
396
- return;
397
- handle.markStopping();
398
- // Unexpose topology services from daemon Netron before stopping processes
399
- if (handle.serviceRouter) {
400
- try {
401
- for (const svcName of handle.serviceRouter.getServiceNames()) {
402
- await handle.serviceRouter.unexposeService(svcName);
403
- }
878
+ /**
879
+ * Everything a handle still runs, stopped: its supervisor's children (or its
880
+ * classic child process) AND its topology pools.
881
+ *
882
+ * The pools were missed. A pool's workers are not the supervisor's children
883
+ * — they are the daemon's, siblings of the server, recorded only in
884
+ * `topologyPools` — so `supervisor.stop()` never reached them, and nothing
885
+ * in the orchestrator destroyed a pool: only the daemon's own shutdown did.
886
+ * Every restart of an app with `instances > 1` left the previous workers
887
+ * running. On sample/test (2026-09-25): quotes's `ohlcv-aggregator` and
888
+ * storage's `transform` in three generations at once.
889
+ *
890
+ * The pools go even when the supervisor's stop throws: a stop that failed
891
+ * half-way must not also leave the half nobody asked about.
892
+ */
893
+ async tearDownProcesses(handle, force = false, timeout = 10_000) {
894
+ try {
895
+ if (handle.mode === 'bootstrap' && handle.supervisor) {
896
+ await handle.supervisor.stop();
404
897
  }
405
- catch {
406
- // Best-effort cleanup
898
+ else if (handle.childProcess) {
899
+ await this.stopChildProcess(handle, force, timeout);
900
+ }
901
+ }
902
+ finally {
903
+ for (const [name, pool] of handle.topologyPools) {
904
+ try {
905
+ await pool.destroy();
906
+ }
907
+ catch (err) {
908
+ this.logger.warn({ app: handle.name, pool: name, error: err.message }, 'Destroying a topology pool failed — its workers may still run');
909
+ }
407
910
  }
408
- handle.serviceRouter = null;
911
+ handle.topologyPools.clear();
409
912
  }
410
- if (handle.mode === 'bootstrap' && handle.supervisor) {
411
- await handle.supervisor.stop();
913
+ }
914
+ /**
915
+ * Give back every service an app exposed on the daemon's Netron.
916
+ *
917
+ * A registration outlives the process behind it: the daemon keeps answering
918
+ * `queryInterface` with the full method list and every call through the name
919
+ * fails at the socket. `takeOverExisting` rescues a name the next launch
920
+ * re-exposes; one it does not stays advertised for the life of the daemon.
921
+ */
922
+ async releaseExposedServices(handle) {
923
+ if (!handle.serviceRouter)
924
+ return;
925
+ try {
926
+ await handle.serviceRouter.releaseAll();
412
927
  }
413
- else if (handle.childProcess) {
414
- await this.stopChildProcess(handle, force, timeout);
928
+ catch (err) {
929
+ this.logger.warn({ app: handle.name, error: err.message }, 'Releasing exposed services failed — the daemon may still advertise them');
415
930
  }
416
- handle.markStopped();
417
- this.persistState();
418
- this.logger.info({ app: name }, 'App stopped');
931
+ handle.serviceRouter = null;
932
+ }
933
+ async stopApp(name, force = false, timeout = 10_000) {
934
+ const canonical = this.resolveAppName(name) ?? name;
935
+ // Outside the queue on purpose: the operator's intent lands the
936
+ // moment the command arrives, not whenever the queue drains.
937
+ this.clearCoolDown(canonical);
938
+ return this.lifecycleQueue.enqueue(canonical, 'stop', async () => {
939
+ const handle = this.handles.get(canonical);
940
+ if (!handle || handle.status === 'stopped')
941
+ return;
942
+ handle.markStopping();
943
+ // Unexpose topology services from daemon Netron before stopping processes
944
+ await this.releaseExposedServices(handle);
945
+ await this.tearDownProcesses(handle, force, timeout);
946
+ handle.markStopped();
947
+ this.persistState();
948
+ this.logger.info({ app: name }, 'App stopped');
949
+ });
419
950
  }
420
951
  async stopAll(force = false) {
421
- if (!this.config)
422
- return 0;
423
952
  if (this.metricsTimer) {
424
953
  clearInterval(this.metricsTimer);
425
954
  this.metricsTimer = null;
@@ -432,7 +961,34 @@ export class OrchestratorService extends EventEmitter {
432
961
  let count = 0;
433
962
  // When force=true (daemon shutdown), use short timeout per app (3s SIGTERM, then SIGKILL)
434
963
  const perAppTimeout = force ? 3_000 : 10_000;
435
- for (const batch of resolveShutdownOrder(this.config.apps)) {
964
+ // Registered projects need not belong to the daemon's root ecosystem.
965
+ // Ownership is defined by handles, not by whichever config booted first.
966
+ // Drain admitted starts before taking the shutdown graph snapshot.
967
+ await Promise.allSettled([...this.startingApps.values()]);
968
+ const entries = [...this.handles.values()].map((handle) => handle.entry);
969
+ const names = new Set(entries.map((entry) => entry.name));
970
+ const graph = entries.map((entry) => {
971
+ const scope = entry.name.slice(0, entry.name.lastIndexOf('/') + 1);
972
+ return {
973
+ ...entry,
974
+ dependsOn: (entry.dependsOn ?? []).flatMap((dependency) => {
975
+ const name = !dependency.includes('/') && names.has(scope + dependency)
976
+ ? scope + dependency : dependency;
977
+ // A dependency never started here has no owned process to stop.
978
+ return names.has(name) ? [name] : [];
979
+ }),
980
+ };
981
+ });
982
+ let batches;
983
+ try {
984
+ batches = resolveShutdownOrder(graph);
985
+ }
986
+ catch (error) {
987
+ // Invalid reconfiguration must not turn shutdown into leaked children.
988
+ this.logger.error({ error: error.message }, 'Invalid shutdown graph; stopping every owned app');
989
+ batches = [entries];
990
+ }
991
+ for (const batch of batches) {
436
992
  await Promise.all(batch.map(async (entry) => {
437
993
  try {
438
994
  await this.stopApp(entry.name, force, perAppTimeout);
@@ -458,8 +1014,28 @@ export class OrchestratorService extends EventEmitter {
458
1014
  this.buildService = null;
459
1015
  }
460
1016
  this.buildResults.clear();
1017
+ this.bootstrapSources.clear();
461
1018
  return count;
462
1019
  }
1020
+ /**
1021
+ * Give a supervised app the definition its next start will use — its
1022
+ * environment, its command. The running process is not touched; the next
1023
+ * `restartApp` starts from this. `false` when nothing by that name runs
1024
+ * here. See `AppHandle.redefine`.
1025
+ */
1026
+ async redefineApp(name, entry) {
1027
+ const handle = this.handles.get(this.resolveAppName(name) ?? name);
1028
+ if (!handle)
1029
+ return false;
1030
+ handle.redefine(entry);
1031
+ if (!this.allowsWatch(entry)) {
1032
+ const state = this.restartCoalescer.get(handle.name);
1033
+ if (state)
1034
+ state.watchSerial = 0;
1035
+ }
1036
+ await this.syncBootstrapWatch(handle.name);
1037
+ return true;
1038
+ }
463
1039
  /**
464
1040
  * Restart an app, coalescing concurrent triggers.
465
1041
  *
@@ -493,16 +1069,76 @@ export class OrchestratorService extends EventEmitter {
493
1069
  * triggers, just continuations — they must not bump or the trailing
494
1070
  * coalesced restart would re-fire forever.
495
1071
  */
496
- async restartAppCoalesced(name, fromExternal) {
1072
+ async restartAppCoalesced(name, fromExternal, cause = 'manual') {
497
1073
  const canonical = this.resolveAppName(name) ?? name;
498
1074
  const handle = this.handles.get(canonical);
499
- if (!handle)
1075
+ if (!handle) {
1076
+ // A PROJECT-QUALIFIED name with no live handle is not this daemon's to
1077
+ // start, and matching it against the daemon's own config starts the
1078
+ // WRONG APP.
1079
+ //
1080
+ // The names collide in practice. omnitron's own `omnitron.config.ts`
1081
+ // declares `main`, `payments`, `messaging`, `storage`, `pricing` — the
1082
+ // same five a downstream stack uses. On 2026-09-10, after payments died,
1083
+ // `omnitron restart acme/dev/payments` matched omnitron's sample entry by
1084
+ // its last path segment and launched `apps/payments/src/main.ts` relative
1085
+ // to the DAEMON's cwd. It died with ERR_MODULE_NOT_FOUND on a path that
1086
+ // exists in neither repository, and the backend stayed down while the
1087
+ // command reported that it had restarted it.
1088
+ //
1089
+ // Starting the wrong app is worse than refusing: the operator is told
1090
+ // their service is coming back, and the log that says otherwise is the
1091
+ // one they are not reading yet. `startApp` on the daemon already refuses
1092
+ // this exact shape and names the command that works; this is the same
1093
+ // refusal on the same reasoning, at the point where "no live handle" is
1094
+ // already known.
1095
+ const qualified = name.includes('/');
1096
+ const ownProject = this.config?.project;
1097
+ if (qualified && !ownProject) {
1098
+ const [project, stack] = name.split('/');
1099
+ throw new Error(`'${name}' belongs to a registered project, which this daemon restarts through its ` +
1100
+ `stack. Use \`omnitron stack start ${project} ${stack}\`, or restart the app by its ` +
1101
+ `bare name if you mean the daemon's own config.`);
1102
+ }
1103
+ // P1-F — fall through to startApp when there's no live handle but
1104
+ // the ecosystem config declares the app. Operators expect
1105
+ // `omnitron restart foo` to work even when `foo` was never started
1106
+ // (or was stopped, or crashed and was reaped). Previously this
1107
+ // threw "Unknown app", forcing the operator to first run start
1108
+ // explicitly.
1109
+ const effective = effectiveAppName(name);
1110
+ const entry = this.config?.apps.find((a) => a.name === name || a.name === effective || effectiveAppName(a.name) === effective);
1111
+ if (entry) {
1112
+ this.logger.info({ app: name }, 'restartApp: no live handle — falling through to startApp');
1113
+ // Restart under the name the OPERATOR asked for. The match above
1114
+ // accepts a bare entry for a qualified request, and handing it to
1115
+ // `startApp` unchanged re-registers the app bare —
1116
+ // `ensureNamespacedEntry` leaves an entry with no OMNITRON_PROJECT /
1117
+ // OMNITRON_STACK env alone by design. The app then silently leaves its
1118
+ // stack, because `ProjectService.toStackInfo` filters handles on the
1119
+ // `${project}/${stack}/` prefix.
1120
+ const requested = name.includes('/') && !entry.name.includes('/')
1121
+ ? { ...entry, name }
1122
+ : entry;
1123
+ return this.startApp(requested);
1124
+ }
500
1125
  throw new Error(`Unknown app: ${name}`);
1126
+ }
501
1127
  name = canonical;
502
1128
  const state = this.getCoalescerState(name);
503
- if (fromExternal)
1129
+ if (fromExternal) {
504
1130
  state.triggerSerial += 1;
505
- const observedSerial = state.triggerSerial;
1131
+ if (cause === 'watch')
1132
+ state.watchSerial = state.triggerSerial;
1133
+ else
1134
+ state.manualSerial = state.triggerSerial;
1135
+ }
1136
+ if (!this.allowsWatch(handle.entry) || ['errored', 'crashed', 'stopped', 'stopping'].includes(handle.status)) {
1137
+ state.watchSerial = 0;
1138
+ }
1139
+ const observedSerial = Math.max(state.manualSerial, state.watchSerial);
1140
+ if (observedSerial <= state.firedSerial)
1141
+ return handle;
506
1142
  // Cooldown: the app reported online recently. Wait it out, then
507
1143
  // proceed as a non-external continuation (we already counted this
508
1144
  // trigger when we first arrived).
@@ -531,7 +1167,7 @@ export class OrchestratorService extends EventEmitter {
531
1167
  }
532
1168
  // Take the slot. Capture which serial this run is consuming —
533
1169
  // anything bumped after this is the next run's problem.
534
- const consumedSerial = state.triggerSerial;
1170
+ const consumedSerial = observedSerial;
535
1171
  const promise = this.restartAppNow(name);
536
1172
  state.inFlight = promise;
537
1173
  try {
@@ -547,7 +1183,7 @@ export class OrchestratorService extends EventEmitter {
547
1183
  // trigger arrived between when we claimed the slot and now.
548
1184
  // Two coalescer arms colliding can no longer cause this branch
549
1185
  // to fire because they don't bump triggerSerial.
550
- if (state.triggerSerial > consumedSerial) {
1186
+ if (Math.max(state.manualSerial, state.watchSerial) > consumedSerial) {
551
1187
  this.restartAppCoalesced(name, /* fromExternal */ false).catch((err) => {
552
1188
  this.logger.error({ app: name, error: err.message }, 'Trailing coalesced restart failed');
553
1189
  });
@@ -569,6 +1205,26 @@ export class OrchestratorService extends EventEmitter {
569
1205
  * bootstrap-config cache is cleared because changes to `bootstrap.ts`
570
1206
  * (topology definition) need to take effect on the next start.
571
1207
  */
1208
+ /**
1209
+ * Start an app this daemon already supervises, using the entry its handle
1210
+ * carries.
1211
+ *
1212
+ * `startApp(entry)` needs an ecosystem entry, which the daemon's own config
1213
+ * is only one source of — a project stack registers handles this daemon
1214
+ * never declared. `restartAppNow` has always taken the entry off the handle
1215
+ * for exactly that reason; this is the same move for a start, so the name
1216
+ * `list` prints is a name `start` accepts.
1217
+ *
1218
+ * Returns `undefined` when the app is unknown, leaving the caller to decide
1219
+ * what to say about a name that names nothing.
1220
+ */
1221
+ async startKnownApp(name) {
1222
+ const canonical = this.resolveAppName(name) ?? name;
1223
+ const handle = this.handles.get(canonical);
1224
+ if (!handle)
1225
+ return undefined;
1226
+ return this.startApp(handle.entry);
1227
+ }
572
1228
  async restartAppNow(name) {
573
1229
  const handle = this.handles.get(name);
574
1230
  if (!handle)
@@ -583,7 +1239,7 @@ export class OrchestratorService extends EventEmitter {
583
1239
  getCoalescerState(name) {
584
1240
  let state = this.restartCoalescer.get(name);
585
1241
  if (!state) {
586
- state = { inFlight: null, triggerSerial: 0, firedSerial: 0, cooldownUntil: 0 };
1242
+ state = { inFlight: null, triggerSerial: 0, firedSerial: 0, manualSerial: 0, watchSerial: 0, cooldownUntil: 0 };
587
1243
  this.restartCoalescer.set(name, state);
588
1244
  }
589
1245
  return state;
@@ -602,10 +1258,14 @@ export class OrchestratorService extends EventEmitter {
602
1258
  * Resolve a user-supplied name to a canonical handle key.
603
1259
  *
604
1260
  * Inputs accepted:
605
- * - exact canonical key (`omni/dev/paysys`) → returned as-is
606
- * - bare short name (`paysys`) → returns the canonical key whose
1261
+ * - exact canonical key (`omni/dev/payments`) → returned as-is
1262
+ * - bare short name (`payments`) → returns the canonical key whose
607
1263
  * last `/`-segment matches, IF AND ONLY IF the match is unique.
608
1264
  *
1265
+ * A qualified name that matches no handle exactly resolves to nothing: it
1266
+ * names a deployment this daemon does not have, and the nearest app with
1267
+ * the same last segment is a different one.
1268
+ *
609
1269
  * Returns `undefined` for no match. Throws on ambiguity (multiple
610
1270
  * stacks expose the same short name) so the caller can surface a
611
1271
  * clear error to the operator instead of operating on the wrong
@@ -614,6 +1274,18 @@ export class OrchestratorService extends EventEmitter {
614
1274
  resolveAppName(rawName) {
615
1275
  if (this.handles.has(rawName))
616
1276
  return rawName;
1277
+ // A qualified name is an ADDRESS, and the only handle that satisfies it
1278
+ // is the one with that address. Falling through to the short-name match
1279
+ // answered a question about one deployment with another's data:
1280
+ //
1281
+ // omnitron logs sample/deployed/main → sample/dev/main
1282
+ //
1283
+ // — the node's app asked for, this laptop's app printed, with nothing
1284
+ // said. `startApp` already refuses this for the same reason ("a
1285
+ // PROJECT-QUALIFIED name with no live handle is not this daemon's to
1286
+ // start"); the resolver every other caller goes through did not.
1287
+ if (rawName.includes('/'))
1288
+ return undefined;
617
1289
  const effective = effectiveAppName(rawName);
618
1290
  const matches = [];
619
1291
  for (const key of this.handles.keys()) {
@@ -667,8 +1339,11 @@ export class OrchestratorService extends EventEmitter {
667
1339
  this.logger.info({ app: name, child: childName }, 'Child reloaded successfully');
668
1340
  }
669
1341
  catch (err) {
670
- this.logger.error({ app: name, child: childName, error: err.message }, 'Failed to reload child — aborting zero-downtime reload');
671
- throw new Error(`Zero-downtime reload failed for '${name}' at child '${childName}': ${err.message}`);
1342
+ // `error: err`, not `error: err.message`: the logger aliases `error`
1343
+ // onto pino's error serializer, so the object arrives with its stack
1344
+ // and a string arrives as a string with nothing behind it.
1345
+ this.logger.error({ app: name, child: childName, error: err }, 'Failed to reload child — aborting zero-downtime reload');
1346
+ throw new Error(`Zero-downtime reload failed for '${name}' at child '${childName}': ${err.message}`, { cause: err });
672
1347
  }
673
1348
  }
674
1349
  this.logger.info({ app: name }, 'Zero-downtime reload complete');
@@ -761,6 +1436,14 @@ export class OrchestratorService extends EventEmitter {
761
1436
  getWorkerHandle(processId) {
762
1437
  return this.pm.getWorkerHandle(processId);
763
1438
  }
1439
+ /**
1440
+ * Surface the underlying IProcessInfo for a titan-pm processId
1441
+ * so callers (e.g. the daemon's `inspect` RPC) can read uptime
1442
+ * + status without reaching into the process-manager directly.
1443
+ */
1444
+ getChildProcessInfo(processId) {
1445
+ return this.pm.getProcess(processId);
1446
+ }
764
1447
  // ============================================================================
765
1448
  // Monitoring — async, delegates to PM supervisor for bootstrap mode
766
1449
  // ============================================================================
@@ -793,38 +1476,23 @@ export class OrchestratorService extends EventEmitter {
793
1476
  }
794
1477
  return null;
795
1478
  }
1479
+ /**
1480
+ * `name` is resolved like every other per-app question (`main` finds
1481
+ * `sample/dev/main`); it was compared verbatim, so `omnitron metrics main`
1482
+ * printed «(no data)». An unknown name answers `{}`, and the caller says so.
1483
+ */
796
1484
  async getMetrics(name) {
1485
+ const wanted = name === undefined ? undefined : this.resolveAppName(name);
1486
+ if (name !== undefined && wanted === undefined)
1487
+ return {};
797
1488
  const result = {};
798
1489
  for (const [appName, handle] of this.handles) {
799
- if (name && appName !== name)
1490
+ if (wanted && appName !== wanted)
800
1491
  continue;
801
1492
  if (handle.mode === 'bootstrap' && handle.supervisor) {
802
- const childNames = handle.supervisor.getChildNames();
803
- let cpu = 0, memory = 0, requests = 0, errors = 0;
804
- for (const childName of childNames) {
805
- // Try PM-level RPC metrics first
806
- const m = await handle.supervisor.getChildMetrics(childName);
807
- if (m && (m.cpu > 0 || m.memory > 0)) {
808
- cpu += m.cpu;
809
- memory += m.memory;
810
- requests += m.requests ?? 0;
811
- errors += m.errors ?? 0;
812
- }
813
- else {
814
- // Fallback: OS-level metrics via ps for the child PID
815
- const processId = handle.supervisor.getChildProcessId(childName);
816
- if (processId) {
817
- const workerHandle = this.pm.getWorkerHandle(processId);
818
- const childPid = workerHandle?.pid;
819
- if (childPid) {
820
- const osMetrics = await this.sampleProcessMetrics(childPid);
821
- cpu += osMetrics.cpu;
822
- memory += osMetrics.memory;
823
- }
824
- }
825
- }
826
- }
827
- const aggregated = { cpu, memory, requests, errors };
1493
+ // Same sampler the poller uses, so `omnitron metrics` and `omnitron
1494
+ // list` cannot report different numbers for the same app.
1495
+ const aggregated = await this.sampleAppMetrics(handle);
828
1496
  handle.lastMetrics = aggregated;
829
1497
  result[appName] = aggregated;
830
1498
  }
@@ -835,21 +1503,36 @@ export class OrchestratorService extends EventEmitter {
835
1503
  }
836
1504
  return result;
837
1505
  }
1506
+ /**
1507
+ * Each app's health, asked of every process it runs — its supervisor
1508
+ * children and its worker pools — and combined (`app-health.ts`).
1509
+ *
1510
+ * `name` is resolved like every other per-app question: `main` finds
1511
+ * `sample/dev/main`. It was compared verbatim, so `omnitron health main`
1512
+ * matched nothing and the answer was empty. An unknown name answers `{}`,
1513
+ * and the caller says so.
1514
+ */
838
1515
  async getHealth(name) {
1516
+ const wanted = name === undefined ? undefined : this.resolveAppName(name);
1517
+ if (name !== undefined && wanted === undefined)
1518
+ return {};
839
1519
  const result = {};
840
1520
  for (const [appName, handle] of this.handles) {
841
- if (name && appName !== name)
1521
+ if (wanted && appName !== wanted)
1522
+ continue;
1523
+ if (handle.status !== 'online') {
1524
+ result[appName] = notRunningHealth(handle.status);
842
1525
  continue;
1526
+ }
843
1527
  if (handle.mode === 'bootstrap' && handle.supervisor) {
844
- const childNames = handle.supervisor.getChildNames();
845
- if (childNames.length > 0) {
846
- const health = await handle.supervisor.getChildHealth(childNames[0]);
847
- handle.lastHealth = health;
848
- result[appName] = health;
849
- }
850
- else {
851
- result[appName] = handle.lastHealth;
1528
+ const supervisor = handle.supervisor;
1529
+ const answers = await Promise.all(supervisor.getChildNames().map(async (child) => [child, await supervisor.getChildHealth(child)]));
1530
+ for (const [pool, instance] of handle.topologyPools) {
1531
+ answers.push([pool, poolHealth(instance.metrics)]);
852
1532
  }
1533
+ const health = answers.length > 0 ? combineProcessHealth(answers) : handle.lastHealth;
1534
+ handle.lastHealth = health;
1535
+ result[appName] = health;
853
1536
  }
854
1537
  else {
855
1538
  result[appName] = handle.lastHealth;
@@ -952,7 +1635,7 @@ export class OrchestratorService extends EventEmitter {
952
1635
  else {
953
1636
  this.logger.error({ app: entry.name, code, signal }, 'App exited unexpectedly');
954
1637
  handle.markCrashed();
955
- this.handleClassicCrash(entry, handle, config);
1638
+ this.scheduleRecovery(entry, handle, config);
956
1639
  }
957
1640
  this.persistState();
958
1641
  });
@@ -967,29 +1650,56 @@ export class OrchestratorService extends EventEmitter {
967
1650
  async launchBootstrapMode(entry, handle, config) {
968
1651
  handle.markStarting();
969
1652
  const bootstrapAbsPath = path.resolve(entry.cwd ?? this.cwd, entry.bootstrap);
970
- // Load bootstrap definition to check for process topology and requirements
1653
+ // Load bootstrap definition to check for process topology and requirements.
1654
+ //
1655
+ // In dev, in a process of its own (`freshModuleGraph`): imported here, a
1656
+ // package the definition uses would be the one this daemon first saw, for
1657
+ // as long as it runs (bootstrap-loader.ts has the measurement). What comes
1658
+ // back is data, which is all the daemon reads of it — here, in the build
1659
+ // service and in the stack resolver; the app's own processes load the
1660
+ // definition themselves, functions and all.
971
1661
  let topology;
972
1662
  let definition;
973
1663
  try {
974
- definition = await loadBootstrapConfig(bootstrapAbsPath, { devMode: this.devMode });
1664
+ definition = await loadBootstrapConfig(bootstrapAbsPath, { devMode: this.devMode, freshModuleGraph: true });
975
1665
  topology = definition.processes;
976
1666
  }
977
1667
  catch (err) {
978
- this.logger.warn({ app: entry.name, error: err.message }, 'Could not load bootstrap config for topology — using single-process mode');
1668
+ // Refused, with the reason. This fell back to «single-process mode» at
1669
+ // warn — but every definition declares its topology (IAppDefinition:
1670
+ // `processes` is required), so the fallback started the app in a shape
1671
+ // it was never written for. Measured 2026-09-23 on the master: payments
1672
+ // and messaging, whose definitions failed to load («Parameter
1673
+ // decorators only work when experimental decorators are enabled»), were
1674
+ // started that way and died on «Dependency 'AuthModule:JWTService' not
1675
+ // found» and a module path that single mode could not resolve — the
1676
+ // operator saw those, and the cause sat one line up at warn.
1677
+ throw new Error(`Could not load ${entry.name}'s definition from ${bootstrapAbsPath}: ${err.message}`, { cause: err });
979
1678
  }
980
- // Populate omnitronConfig from app's config/default.json if not set by bootstrap
981
- if (definition && !definition.omnitronConfig) {
982
- try {
983
- const srcDir = path.dirname(bootstrapAbsPath);
984
- const appRoot = path.resolve(srcDir, '..');
985
- const configPath = path.join(appRoot, 'config', 'default.json');
986
- const content = fs.readFileSync(configPath, 'utf-8');
987
- const json = JSON.parse(content);
988
- if (json.omnitron) {
989
- definition.omnitronConfig = json.omnitron;
1679
+ // The app's `config/default.json`, read on every start and put on a COPY
1680
+ // of the definition. This wrote the section onto the loader's cached
1681
+ // definition «if not set», so the first start's reading was every later
1682
+ // start's — and the logger level beside it, read in the same block, was
1683
+ // never read again at all (`project/declared-config.ts`).
1684
+ if (definition) {
1685
+ const declared = readDeclaredConfig(bootstrapAbsPath);
1686
+ if (declared.malformed) {
1687
+ this.logger.error({ app: entry.name, configPath: declared.configPath, error: declared.malformed }, 'config/default.json does not parse — its `omnitron` section is being ignored');
1688
+ }
1689
+ definition = withDeclaredConfig(definition, declared);
1690
+ // The only place the parent can learn the level the app chose; children
1691
+ // are spawned with it below. Validated rather than trusted: this is a
1692
+ // hand-edited JSON file, and a level pino does not know ("verbose", a
1693
+ // typo) would be handed straight to a child's logger constructor.
1694
+ const level = declared.loggerLevel;
1695
+ if (typeof level === 'string') {
1696
+ if (PINO_LEVELS.has(level)) {
1697
+ handle.appLogLevel = level;
1698
+ }
1699
+ else {
1700
+ this.logger.warn({ app: entry.name, configPath: declared.configPath, level }, 'config/default.json declares a logger level this runtime does not know — children keep the default');
990
1701
  }
991
1702
  }
992
- catch { /* config file missing — skip */ }
993
1703
  }
994
1704
  // Resolve infrastructure config and inject as env vars.
995
1705
  // Skip if already injected by project.service.ts (stack flow sets OMNITRON_STACK).
@@ -1020,93 +1730,8 @@ export class OrchestratorService extends EventEmitter {
1020
1730
  }
1021
1731
  // Build all entry points via esbuild (dev mode bundles from source .ts)
1022
1732
  //
1023
- // On a *restart* (as opposed to a first start), buildResults already
1024
- // has a fresh entry — esbuild's watcher just produced the bundle
1025
- // that triggered us. Reusing it skips a redundant cold rebuild
1026
- // (which was costing ~500ms per restart and racing against the
1027
- // watcher's own incremental output). The watch context is also
1028
- // preserved (we never unwatched), so we don't re-arm it either.
1029
- if (definition && this.devMode) {
1030
- try {
1031
- if (!this.buildService) {
1032
- this.buildService = new BuildService(/* isDev */ true, {
1033
- onEsbuildReset: (info) => this.metricsBridge?.recordResilientHandleReset(info),
1034
- onBuildResult: ({ app, kind, durationMs, ok }) => this.metricsBridge?.recordEsbuildBuild(app, kind, durationMs, ok),
1035
- });
1036
- }
1037
- const existing = this.buildResults.get(entry.name);
1038
- const existingWatch = this.buildService.isWatching?.(entry.name);
1039
- // Bundle on-disk presence check. A restart can race against
1040
- // the user (or a script) removing `.omnitron-build/` between
1041
- // builds — without this stat the orchestrator hands the
1042
- // worker a path that no longer exists and the child dies
1043
- // with `ERR_MODULE_NOT_FOUND` before the supervisor can
1044
- // recover. Cheap stat, big DX win.
1045
- const bundlePresent = existing
1046
- ? fs.existsSync(existing.bootstrapPath)
1047
- : false;
1048
- if (existing && existingWatch && bundlePresent) {
1049
- // Restart path — keep the already-built bundle and live watch.
1050
- this.logger.debug({ app: entry.name, modules: existing.modulePaths.size }, 'Reusing existing esbuild bundle + watch (restart path)');
1051
- }
1052
- else {
1053
- if (existing && existingWatch && !bundlePresent) {
1054
- // Bundle was deleted out from under us. Clear the watch
1055
- // and the cached BuildResult so the next call rebuilds
1056
- // from cold and re-arms the watcher cleanly.
1057
- this.logger.warn({ app: entry.name, bootstrapPath: existing.bootstrapPath }, 'Cached bundle missing on disk — forcing rebuild');
1058
- try {
1059
- await this.buildService.unwatchApp?.(entry.name);
1060
- }
1061
- catch (err) {
1062
- this.logger.debug({ err, app: entry.name }, 'unwatchApp failed (non-fatal)');
1063
- }
1064
- this.buildResults.delete(entry.name);
1065
- }
1066
- // First-start path — build everything from cold and arm the watcher.
1067
- const buildResult = await this.buildService.buildApp(entry.name, bootstrapAbsPath, definition);
1068
- this.buildResults.set(entry.name, buildResult);
1069
- this.logger.info({ app: entry.name, modules: buildResult.modulePaths.size }, 'Built entry points via esbuild');
1070
- // Start esbuild watch for incremental rebuilds on source changes.
1071
- // On rebuild → fire a restart trigger; the coalescer takes it
1072
- // from there.
1073
- await this.buildService.watchApp(entry.name, bootstrapAbsPath, definition, () => {
1074
- // Gate: skip if the app is in a non-running state — repeated
1075
- // restarts of an escalated/errored process just churn forever
1076
- // without forward progress (Fix #248).
1077
- // Skip restart when the app is in a terminal non-running
1078
- // state. Includes 'crashed' (supervisor escalated after
1079
- // exceeding its restart budget) and 'errored' (start
1080
- // failed). 'stopped' means operator explicitly stopped it.
1081
- // In all three cases, restarting just churns a known-broken
1082
- // process; the operator must fix the underlying issue and
1083
- // call `omnitron start <app>` to clear the state.
1084
- const status = this.getAppStatus(entry.name);
1085
- if (status === 'errored' || status === 'crashed' || status === 'stopped') {
1086
- this.logger.warn({ app: entry.name, status }, 'esbuild rebuild detected but app is not running — skipping restart');
1087
- return;
1088
- }
1089
- this.logger.info({ app: entry.name }, 'esbuild rebuild detected — restarting');
1090
- this.metricsBridge?.recordEsbuildBuild(entry.name, 'rebuild', 0, true);
1091
- this.restartApp(entry.name).catch((err) => {
1092
- this.logger.error({ app: entry.name, error: err.message }, 'Restart after esbuild rebuild failed');
1093
- });
1094
- });
1095
- }
1096
- }
1097
- catch (err) {
1098
- // The "fall back to tsx" wording was misleading — the orchestrator
1099
- // does not inject a tsx loader into spawned children, so without
1100
- // a build the bootstrap-process will try to `import` the source
1101
- // .ts file (referenced as `./app.module.js` in the topology) and
1102
- // fail with a cryptic "Cannot find module" downstream. Surface
1103
- // the build error directly so the operator sees the actual
1104
- // problem on `omnitron start/restart`.
1105
- this.logger.error({ app: entry.name, error: err.message }, 'esbuild build failed — app cannot start without bundled output');
1106
- throw new Error(`Cannot launch '${entry.name}' in dev mode: esbuild build failed (${err.message}). ` +
1107
- `Fix the build error and retry, or run with bundled output present in apps/${entry.name}/.omnitron-build/.`);
1108
- }
1109
- }
1733
+ if (definition)
1734
+ await this.prepareBootstrapBuild(entry, bootstrapAbsPath, definition);
1110
1735
  const buildResult = this.buildResults.get(entry.name);
1111
1736
  // Use bundled bootstrap path for topology import when available
1112
1737
  const effectiveBootstrapPath = buildResult?.bootstrapPath ?? bootstrapAbsPath;
@@ -1117,6 +1742,36 @@ export class OrchestratorService extends EventEmitter {
1117
1742
  await this.launchSingleProcess(entry, handle, config, effectiveBootstrapPath, buildResult);
1118
1743
  }
1119
1744
  }
1745
+ async prepareBootstrapBuild(entry, bootstrapAbsPath, definition) {
1746
+ const sourceTopology = /\.(?:[cm]?ts|tsx)$/.test(bootstrapAbsPath) ||
1747
+ definition.processes.some((process) => /\.(?:[cm]?ts|tsx)$/.test(process.module));
1748
+ if (!this.devMode && !sourceTopology)
1749
+ return;
1750
+ try {
1751
+ this.buildService ??= new BuildService(this.devMode, {
1752
+ onEsbuildReset: (info) => this.metricsBridge?.recordResilientHandleReset(info),
1753
+ onBuildResult: ({ app, kind, durationMs, ok }) => this.metricsBridge?.recordEsbuildBuild(app, kind, durationMs, ok),
1754
+ });
1755
+ const existing = this.buildResults.get(entry.name);
1756
+ const current = this.handles.get(entry.name)?.entry ?? entry;
1757
+ const reusable = existing && this.allowsWatch(current) && this.buildService.isWatching(entry.name) &&
1758
+ this.buildService.canReuseBuild(existing, bootstrapAbsPath, definition);
1759
+ if (!reusable) {
1760
+ // A manual restart with watch:false deliberately compiles the current
1761
+ // sources. It must not reuse an artifact that stopped being maintained.
1762
+ await this.buildService.unwatchApp(entry.name);
1763
+ this.buildResults.delete(entry.name);
1764
+ this.buildResults.set(entry.name, await this.buildService.buildApp(entry.name, bootstrapAbsPath, definition));
1765
+ this.logger.info({ app: entry.name }, 'Built entry points via esbuild');
1766
+ }
1767
+ this.bootstrapSources.set(entry.name, { path: bootstrapAbsPath, definition });
1768
+ await this.syncBootstrapWatch(entry.name);
1769
+ }
1770
+ catch (error) {
1771
+ this.logger.error({ app: entry.name, error }, 'esbuild build failed — app cannot start without bundled output');
1772
+ throw new Error(`Cannot launch '${entry.name}': esbuild build failed (${error.message}). Fix the build error and retry.`, { cause: error });
1773
+ }
1774
+ }
1120
1775
  /**
1121
1776
  * Launch app with process topology — multiple children managed by Omnitron.
1122
1777
  *
@@ -1157,14 +1812,20 @@ export class OrchestratorService extends EventEmitter {
1157
1812
  size: instances,
1158
1813
  strategy: PoolStrategy.POWER_OF_TWO,
1159
1814
  metrics: true,
1160
- replaceUnhealthy: true,
1161
- requestTimeout: 120_000,
1815
+ // `replaceUnhealthy` is deliberately not set: titan-pm declares it
1816
+ // and reads it nowhere. An unhealthy worker is replaced by the
1817
+ // health path regardless, so `true` was a no-op that happened to
1818
+ // match the behaviour — and `false` would have been a no-op that did
1819
+ // not, leaving an operator believing they had turned replacement off.
1820
+ // What actually governs it is `healthCheck.unhealthyThreshold`.
1821
+ requestTimeout: CHILD_REQUEST_TIMEOUT,
1162
1822
  maxQueueSize: 200,
1163
1823
  spawnOptions: {
1164
1824
  name: `${entry.name}/${procEntry.name}`,
1165
1825
  version: '1.0.0',
1166
1826
  allMethodsPublic: true,
1167
- startupTimeout: procEntry.startupTimeout ?? 60_000,
1827
+ ...(handle.appLogLevel && { logLevel: handle.appLogLevel }),
1828
+ startupTimeout: procEntry.startupTimeout ?? entry.startupTimeout ?? DEFAULT_CHILD_STARTUP_TIMEOUT,
1168
1829
  ...(entry.env && { env: entry.env }),
1169
1830
  ...(entry.cwd && { cwd: entry.cwd }),
1170
1831
  dependencies: {
@@ -1187,25 +1848,31 @@ export class OrchestratorService extends EventEmitter {
1187
1848
  enabled: procEntry.health?.enabled !== false,
1188
1849
  interval: procEntry.health?.interval ?? config.monitoring.healthCheck.interval,
1189
1850
  },
1851
+ // `scaling.strategy` is what opts a pool into autoscaling; anything
1852
+ // else — including the default of not declaring it — means the pool
1853
+ // stays at the declared `instances`, which is what 'fixed' says on
1854
+ // the tin.
1855
+ //
1856
+ // Both arms of this ternary used to set `enabled: true`, differing
1857
+ // only in where the numbers came from. The setting existed, the
1858
+ // mechanism existed, and nothing connected them: every pool in the
1859
+ // platform was autoscaled whether or not it asked to be. Live effect
1860
+ // on two pools declared `instances: 2` — grown to their cap of eight
1861
+ // and held there, sixteen processes where four were configured.
1862
+ //
1863
+ // `min` follows the declared instance count rather than 1: a pool
1864
+ // asked for two workers should not be scaled below two.
1190
1865
  autoScale: procEntry.scaling?.strategy === 'auto'
1191
1866
  ? {
1192
1867
  enabled: true,
1193
- min: 1,
1868
+ min: instances,
1194
1869
  max: procEntry.scaling?.maxInstances ?? instances * 4,
1195
1870
  targetCPU: procEntry.scaling?.targetCPU ?? 70,
1196
1871
  targetMemory: procEntry.scaling?.targetMemory ?? 80,
1197
1872
  queueThreshold: procEntry.scaling?.queueThreshold ?? 10,
1198
1873
  cooldownPeriod: procEntry.scaling?.cooldownPeriod ?? 30_000,
1199
1874
  }
1200
- : {
1201
- enabled: true,
1202
- min: 1,
1203
- max: instances * 4,
1204
- targetCPU: 70,
1205
- targetMemory: 80,
1206
- queueThreshold: 10,
1207
- cooldownPeriod: 30_000,
1208
- },
1875
+ : { enabled: false, min: instances, max: instances },
1209
1876
  };
1210
1877
  this.logger.info({ app: entry.name, process: procEntry.name, instances }, 'Creating topology process pool');
1211
1878
  // pm.pool() returns a Proxy wrapping ProcessPool — execute() dispatches to pool workers
@@ -1217,24 +1884,50 @@ export class OrchestratorService extends EventEmitter {
1217
1884
  // which returns null for pool workers (they aren't supervisor
1218
1885
  // children) and the entry is mis-reported as `status: 'stopped'`.
1219
1886
  handle.topologyPools.set(procEntry.name, pool);
1887
+ this.attachPoolLogCapture(entry.name, procEntry.name, handle, pool);
1220
1888
  // Auto-discover @Service metadata from pool workers and expose via ServiceRouter.
1221
1889
  // No manual `exports` needed — @Service decorator IS the source of truth.
1222
1890
  // Requires explicit `topology: { expose: true }` — opt-in for security.
1223
1891
  if (procEntry.topology?.expose === true) {
1224
1892
  try {
1893
+ // A pool worker reports ready before its Application has started, and
1894
+ // `@Service` classes are exposed to Netron during start — so asking
1895
+ // once, or once more 500ms later, asks a worker that is still booting.
1896
+ // What it answers then is an empty list, or a service whose method
1897
+ // list has not been filled in yet, and both used to end the same way:
1898
+ // nothing registered on the daemon, the consumer process starting
1899
+ // seconds later, querying, and being told the service does not exist.
1900
+ // The consumer resolves its topology proxy exactly once, so that
1901
+ // answer disabled the feature for the life of the process — in the
1902
+ // case that surfaced this, every OHLCV candle aggregation.
1903
+ //
1904
+ // Poll instead, and treat "a service with no methods" as not ready
1905
+ // rather than as a service, because an empty method list produces a
1906
+ // proxy that can do nothing.
1907
+ const usable = (list) => Array.isArray(list) && list.length > 0 && list.some((svc) => svc.methods?.length > 0);
1225
1908
  let services = await pool.execute('getExposedServices');
1226
- // Defensive: if services list is empty, the worker's Application may still
1227
- // be registering services. Retry once after a short delay.
1228
- if (!services || services.length === 0) {
1229
- await new Promise((r) => setTimeout(r, 500));
1909
+ const deadline = Date.now() + TOPOLOGY_EXPOSE_TIMEOUT_MS;
1910
+ while (!usable(services) && Date.now() < deadline) {
1911
+ await new Promise((r) => setTimeout(r, TOPOLOGY_EXPOSE_POLL_MS));
1230
1912
  services = await pool.execute('getExposedServices');
1231
1913
  }
1232
- if (!services || services.length === 0) {
1233
- this.logger.warn({ app: entry.name, process: procEntry.name }, 'No @Service found on pool worker — topology.expose has no effect');
1914
+ if (!usable(services)) {
1915
+ this.logger.warn({
1916
+ app: entry.name,
1917
+ process: procEntry.name,
1918
+ waitedMs: TOPOLOGY_EXPOSE_TIMEOUT_MS,
1919
+ reported: services?.map((svc) => `${svc.name}(${svc.methods?.length ?? 0})`) ?? [],
1920
+ }, 'No usable @Service found on pool worker — topology.expose has no effect, and any ' +
1921
+ 'process declaring topology.access on it will start without a proxy');
1234
1922
  }
1235
- for (const svc of services) {
1236
- if (svc.methods.length === 0)
1923
+ for (const svc of services ?? []) {
1924
+ if (svc.methods.length === 0) {
1925
+ // Silently skipped before. A consumer then failed to find a
1926
+ // service its own declaration named, with nothing anywhere
1927
+ // connecting the two facts.
1928
+ this.logger.warn({ app: entry.name, process: procEntry.name, service: svc.name }, 'Pool service exposes no methods — not registered on the daemon');
1237
1929
  continue;
1930
+ }
1238
1931
  await serviceRouter.exposePoolService(procEntry.name, svc.name, svc.version ?? '1.0.0', pool, svc.methods);
1239
1932
  this.logger.info({ app: entry.name, process: procEntry.name, service: svc.name, methods: svc.methods.length }, 'Auto-discovered and exposed pool service on daemon Netron');
1240
1933
  }
@@ -1253,7 +1946,10 @@ export class OrchestratorService extends EventEmitter {
1253
1946
  name: `${entry.name}/${procEntry.name}`,
1254
1947
  version: '1.0.0',
1255
1948
  allMethodsPublic: true,
1256
- startupTimeout: procEntry.startupTimeout ?? entry.startupTimeout ?? 30_000,
1949
+ // Without this the child's client takes netron's 5000 ms; the pool
1950
+ // path beside it has always stated one. See CHILD_REQUEST_TIMEOUT.
1951
+ requestTimeout: CHILD_REQUEST_TIMEOUT,
1952
+ startupTimeout: procEntry.startupTimeout ?? entry.startupTimeout ?? DEFAULT_CHILD_STARTUP_TIMEOUT,
1257
1953
  ...(entry.env && { env: entry.env }),
1258
1954
  ...(entry.cwd && { cwd: entry.cwd }),
1259
1955
  dependencies: {
@@ -1271,6 +1967,7 @@ export class OrchestratorService extends EventEmitter {
1271
1967
  timeout: procEntry.health?.timeout ?? config.monitoring.healthCheck.timeout,
1272
1968
  },
1273
1969
  observability: { metrics: true },
1970
+ ...(handle.appLogLevel && { logLevel: handle.appLogLevel }),
1274
1971
  };
1275
1972
  children.push({
1276
1973
  name: `${entry.name}/${procEntry.name}`,
@@ -1295,18 +1992,11 @@ export class OrchestratorService extends EventEmitter {
1295
1992
  };
1296
1993
  // Create supervisor WITHOUT starting — wire events first to avoid race condition
1297
1994
  const supervisor = this.pm.createSupervisor(supervisorConfig);
1298
- supervisor.on('child:started', (childName) => {
1299
- const processId = supervisor.getChildProcessId(childName);
1300
- if (processId) {
1301
- const workerHandle = this.pm.getWorkerHandle(processId);
1302
- const info = this.pm.getProcess(processId);
1303
- const childPid = workerHandle?.pid ?? info?.pid;
1304
- handle.markOnline(childPid ?? process.pid);
1305
- }
1306
- this.persistState();
1307
- this.startPostOnlineCooldown(entry.name);
1308
- this.emit('app:online', entry.name);
1309
- });
1995
+ // The app's pid is its server's: the single process with a transport, the
1996
+ // one `handle.port` names. A topology with none has no pid to give.
1997
+ const server = singleEntries.find((p) => p.transports?.http) ?? singleEntries.find((p) => p.transports) ?? null;
1998
+ handle.declareChildren(children.map((child) => child.name), server ? `${entry.name}/${server.name}` : null);
1999
+ this.onChildStarted(entry, handle, supervisor);
1310
2000
  // Set supervisor on handle BEFORE start so attachLogCapture can access it
1311
2001
  handle.supervisor = supervisor;
1312
2002
  handle.serviceRouter = serviceRouter;
@@ -1315,6 +2005,73 @@ export class OrchestratorService extends EventEmitter {
1315
2005
  this.wireSupervisorEvents(entry, handle, supervisor);
1316
2006
  // Now start — events will be captured correctly
1317
2007
  await supervisor.start();
2008
+ // Step 4: register the single processes that declare `topology.expose`.
2009
+ //
2010
+ // This was a pool-only feature, ignored in silence everywhere else: a
2011
+ // process declaring `expose: true` got no registration and no warning,
2012
+ // and every consumer naming it in `topology.access` started without a
2013
+ // proxy. Measured on quotes, whose `collector` is a single process
2014
+ // holding the only objects that know whether the exchange WebSockets are
2015
+ // up — the server process could not ask, and answered `ready: down —
2016
+ // exchanges unavailable` while three were connected.
2017
+ //
2018
+ // After `supervisor.start()` because there is no child to ask before it.
2019
+ // A consumer among these children may therefore start before the service
2020
+ // is registered; consumers re-resolve their proxy on use for exactly that
2021
+ // reason (see quotes's `OhlcvSchedulerService`).
2022
+ await this.exposeChildTopologyServices(entry, supervisor, serviceRouter, singleEntries);
2023
+ }
2024
+ /**
2025
+ * Register each single process's `@Service` classes on the daemon.
2026
+ *
2027
+ * Mirrors the pool path above, including its poll: a child reports ready
2028
+ * before its Application has started, and `@Service` classes are exposed to
2029
+ * Netron during start, so asking once asks a process that is still booting
2030
+ * and gets an empty list back.
2031
+ */
2032
+ async exposeChildTopologyServices(entry, supervisor, serviceRouter, singleEntries) {
2033
+ for (const procEntry of singleEntries) {
2034
+ if (procEntry.topology?.expose !== true)
2035
+ continue;
2036
+ const childName = `${entry.name}/${procEntry.name}`;
2037
+ const getProxy = () => supervisor.getChildProxy(childName);
2038
+ try {
2039
+ const usable = (list) => Array.isArray(list) && list.length > 0 && list.some((svc) => svc.methods?.length > 0);
2040
+ const ask = async () => {
2041
+ const child = getProxy();
2042
+ if (!child?.getExposedServices)
2043
+ return undefined;
2044
+ return (await child.getExposedServices());
2045
+ };
2046
+ let services = await ask();
2047
+ const deadline = Date.now() + TOPOLOGY_EXPOSE_TIMEOUT_MS;
2048
+ while (!usable(services) && Date.now() < deadline) {
2049
+ await new Promise((r) => setTimeout(r, TOPOLOGY_EXPOSE_POLL_MS));
2050
+ services = await ask();
2051
+ }
2052
+ if (!usable(services)) {
2053
+ this.logger.warn({
2054
+ app: entry.name,
2055
+ process: procEntry.name,
2056
+ waitedMs: TOPOLOGY_EXPOSE_TIMEOUT_MS,
2057
+ reported: services?.map((svc) => `${svc.name}(${svc.methods?.length ?? 0})`) ?? [],
2058
+ }, 'No usable @Service found on this process — topology.expose has no effect, and any ' +
2059
+ 'process declaring topology.access on it will start without a proxy');
2060
+ continue;
2061
+ }
2062
+ for (const svc of services ?? []) {
2063
+ if (svc.methods.length === 0) {
2064
+ this.logger.warn({ app: entry.name, process: procEntry.name, service: svc.name }, 'Process service exposes no methods — not registered on the daemon');
2065
+ continue;
2066
+ }
2067
+ await serviceRouter.exposeChildService(procEntry.name, svc.name, svc.version ?? '1.0.0', getProxy, svc.methods);
2068
+ this.logger.info({ app: entry.name, process: procEntry.name, service: svc.name, methods: svc.methods.length }, 'Auto-discovered and exposed process service on daemon Netron');
2069
+ }
2070
+ }
2071
+ catch (err) {
2072
+ this.logger.error({ app: entry.name, process: procEntry.name, error: err.message }, "Failed to auto-discover/expose this process's services");
2073
+ }
2074
+ }
1318
2075
  }
1319
2076
  /**
1320
2077
  * Launch single-process bootstrap app (no topology — original behavior).
@@ -1330,11 +2087,22 @@ export class OrchestratorService extends EventEmitter {
1330
2087
  const spawnOpts = {
1331
2088
  name: entry.name,
1332
2089
  version: '1.0.0',
2090
+ // As above: a single child's client otherwise runs on netron's default.
2091
+ requestTimeout: CHILD_REQUEST_TIMEOUT,
1333
2092
  ...(entry.env && { env: entry.env }),
1334
2093
  dependencies: {
1335
2094
  bootstrapPath: bootstrapAbsPath,
1336
2095
  ...(bundledModulePath && { bundledModulePath }),
1337
2096
  },
2097
+ // Omitted when unset, which delegates the budget to titan-pm —
2098
+ // `process-spawner.ts`: `options.startupTimeout ?? resources?.timeout ??
2099
+ // 30_000`. Worth naming, because it is otherwise invisible from here:
2100
+ // an APP that declares nothing gets 30s while its CHILD processes get
2101
+ // `DEFAULT_CHILD_STARTUP_TIMEOUT`, 60s. Backwards if anything, since the
2102
+ // app usually does the heavier work — and the downstream project's own
2103
+ // config carries a note about a 30s default that kept a whole stack from
2104
+ // starting. Left as delegation rather than changed here: raising it is a
2105
+ // decision about titan-pm's contract, not about this call site.
1338
2106
  ...(entry.startupTimeout != null && { startupTimeout: entry.startupTimeout }),
1339
2107
  health: {
1340
2108
  enabled: true,
@@ -1342,6 +2110,7 @@ export class OrchestratorService extends EventEmitter {
1342
2110
  timeout: config.monitoring.healthCheck.timeout,
1343
2111
  },
1344
2112
  observability: { metrics: true },
2113
+ ...(handle.appLogLevel && { logLevel: handle.appLogLevel }),
1345
2114
  };
1346
2115
  const childConfig = {
1347
2116
  name: entry.name,
@@ -1354,7 +2123,8 @@ export class OrchestratorService extends EventEmitter {
1354
2123
  size: instances,
1355
2124
  strategy: PoolStrategy.ROUND_ROBIN,
1356
2125
  metrics: true,
1357
- replaceUnhealthy: true,
2126
+ // See the note on the other pool: `replaceUnhealthy` is declared by
2127
+ // titan-pm and read by nothing.
1358
2128
  healthCheck: {
1359
2129
  enabled: true,
1360
2130
  interval: config.monitoring.healthCheck.interval,
@@ -1392,20 +2162,10 @@ export class OrchestratorService extends EventEmitter {
1392
2162
  };
1393
2163
  // Create supervisor WITHOUT starting — wire events first to avoid race condition
1394
2164
  const supervisor = this.pm.createSupervisor(supervisorConfig);
1395
- // Wire supervisor events BEFORE start so we catch the initial child:started
1396
- supervisor.on('child:started', (childName) => {
1397
- const processId = supervisor.getChildProcessId(childName);
1398
- if (processId) {
1399
- // Prefer WorkerHandle.pid (actual child OS PID), fallback to IProcessInfo.pid
1400
- const workerHandle = this.pm.getWorkerHandle(processId);
1401
- const info = this.pm.getProcess(processId);
1402
- const childPid = workerHandle?.pid ?? info?.pid;
1403
- handle.markOnline(childPid ?? process.pid);
1404
- }
1405
- this.persistState();
1406
- this.startPostOnlineCooldown(entry.name);
1407
- this.emit('app:online', entry.name);
1408
- });
2165
+ // Wire supervisor events BEFORE start so we catch the initial child:started.
2166
+ // One child, and it is the whole app — so it is the server too.
2167
+ handle.declareChildren([childConfig.name], childConfig.name);
2168
+ this.onChildStarted(entry, handle, supervisor);
1409
2169
  // Set supervisor on handle BEFORE start so attachLogCapture can access it
1410
2170
  handle.supervisor = supervisor;
1411
2171
  handle.instanceCount = instances;
@@ -1413,14 +2173,50 @@ export class OrchestratorService extends EventEmitter {
1413
2173
  // Now start — events will be captured correctly
1414
2174
  await supervisor.start();
1415
2175
  }
2176
+ /**
2177
+ * A child's start, for both launch paths: the app is online when the start
2178
+ * completes it, and not before (`AppHandle.childStarted`).
2179
+ *
2180
+ * Each path had its own copy of this handler, and both marked the app
2181
+ * online — with that child's pid — on every child's start. `app:online`
2182
+ * went out once per child with it: three STARTED events for one boot of a
2183
+ * three-process app, the first two about an app still coming up.
2184
+ */
2185
+ onChildStarted(entry, handle, supervisor) {
2186
+ supervisor.on('child:started', (childName) => {
2187
+ // A pool child's proxy answers every property with a function —
2188
+ // `__processId` included — so only a string is a process id. The old
2189
+ // handler took that function for one, found no worker behind it, and
2190
+ // gave such an app the DAEMON's pid.
2191
+ const processId = supervisor.getChildProcessId(childName);
2192
+ const pid = typeof processId === 'string'
2193
+ ? (this.pm.getWorkerHandle(processId)?.pid ?? this.pm.getProcess(processId)?.pid ?? null)
2194
+ : null;
2195
+ const whole = handle.childStarted(childName, pid);
2196
+ this.persistState();
2197
+ if (!whole)
2198
+ return;
2199
+ this.startPostOnlineCooldown(entry.name);
2200
+ this.emit('app:online', entry.name);
2201
+ });
2202
+ }
1416
2203
  /**
1417
2204
  * Wire common supervisor events (crash, restart, escalate) and log capture.
1418
2205
  */
1419
2206
  wireSupervisorEvents(entry, handle, supervisor) {
1420
2207
  supervisor.on('child:started', (childName) => {
1421
2208
  this.attachLogCapture(entry.name, childName, handle);
2209
+ this.attachExitCapture(entry.name, childName, handle);
2210
+ // The app re-entered a live state — drop the previous crash
2211
+ // context so a future `omnitron inspect` shows the *next*
2212
+ // crash, not the last one.
2213
+ handle.clearLastExit();
1422
2214
  });
2215
+ // A child that is down leaves the app partial until it is back — a
2216
+ // sibling's restart must not bring the app `online` around it.
2217
+ supervisor.on('child:stopped', (childName) => handle.childGone(childName));
1423
2218
  supervisor.on('child:crash', (childName, error) => {
2219
+ handle.childGone(childName);
1424
2220
  // If app never reached 'online', mark as errored (not crashed — it never ran)
1425
2221
  if (handle.status === 'starting') {
1426
2222
  handle.markErrored();
@@ -1428,10 +2224,94 @@ export class OrchestratorService extends EventEmitter {
1428
2224
  else {
1429
2225
  handle.markCrashed();
1430
2226
  }
2227
+ // Stash the supervisor-provided error message onto whatever
2228
+ // exit context the workerHandle.onExit hook already wrote. If
2229
+ // the exit event hasn't fired yet (race) we still need to
2230
+ // persist *something* — record a synthetic entry that the
2231
+ // exit hook below will overwrite when it does fire.
2232
+ if (handle.lastExit) {
2233
+ handle.lastExit.message = error.message;
2234
+ }
2235
+ else {
2236
+ handle.recordExit({ code: null, signal: null, expected: false, message: error.message });
2237
+ }
1431
2238
  this.logger.error({ app: entry.name, child: childName, error: error.message }, 'Process crashed');
1432
2239
  this.persistState();
1433
2240
  this.emit('app:crash', entry.name, error);
1434
2241
  });
2242
+ // T#60 — startup failures (child throws/rejects in waitForReady
2243
+ // before ever reporting ready) come through this event, NOT
2244
+ // child:crash. Without a handler the orchestrator just sees
2245
+ // status='errored' with no clue what went wrong. titan-pm
2246
+ // already stuffs the captured stderr into `error.details.stderr`
2247
+ // so we surface it on `lastExit.stderrTail`.
2248
+ supervisor.on('child:start-failed', (childName, error) => {
2249
+ const details = error.details;
2250
+ const stderrTail = typeof details?.stderr === 'string'
2251
+ ? details.stderr.split('\n').filter((l) => l.length > 0).slice(-80)
2252
+ : [];
2253
+ for (const line of stderrTail)
2254
+ handle.appendStderr(line);
2255
+ // A child that never came up because WE stopped the app is not a
2256
+ // failure, and reporting it as one is worse than saying nothing: the
2257
+ // message titan-pm builds for an aborted start says the process "did not
2258
+ // choose to exit, so its own logs will not explain this" — true, and it
2259
+ // sends the reader hunting a fault in an application that was doing
2260
+ // nothing wrong. Every `stack stop` issued while anything was still
2261
+ // booting wrote one of these per child, at error level, into the app's
2262
+ // own log.
2263
+ //
2264
+ // `'stopping'` only — NOT `'stopped'`, which is also an AppHandle's
2265
+ // initial value, so accepting it would swallow the first genuine failure
2266
+ // of any app whose handle had not yet been marked starting.
2267
+ const expected = handle.status === 'stopping' ||
2268
+ error.stoppedDuringStartup === true;
2269
+ if (expected) {
2270
+ handle.recordExit({
2271
+ code: null,
2272
+ signal: null,
2273
+ expected: true,
2274
+ message: error.message,
2275
+ });
2276
+ this.logger.info({ app: entry.name, child: childName, err: error.message }, 'Process stopped before it finished starting');
2277
+ this.persistState();
2278
+ return;
2279
+ }
2280
+ // Put what the child printed into the app's LOG FILE, not only into the
2281
+ // in-memory ring `omnitron inspect` reads.
2282
+ //
2283
+ // A child that dies during startup never produces a WorkerHandle at all
2284
+ // — titan-pm registers one only after `waitForReady` resolves — so there
2285
+ // is no `onLog` to subscribe to here and nothing to replay. What does
2286
+ // exist is the output `waitForReady` captured while waiting, which it
2287
+ // attaches to the error. Feeding that to the log handlers is the only
2288
+ // way a failed boot reaches disk, and a failed boot is the case where
2289
+ // reading it matters most: an application that dies before its logger
2290
+ // exists has said everything it is ever going to say on these streams.
2291
+ // The failure itself first. A child can fail without printing a word —
2292
+ // it reports the error over the IPC channel and exits, which is what a
2293
+ // module that throws on import does — so the streams below are often
2294
+ // empty and the only account of what happened is on the error.
2295
+ this.persistChildFailure(entry.name, childName, error);
2296
+ this.persistChildOutput(entry.name, childName, details?.stdout, 'stdout');
2297
+ this.persistChildOutput(entry.name, childName, details?.stderr, 'stderr');
2298
+ handle.recordExit({
2299
+ code: null,
2300
+ signal: null,
2301
+ expected: false,
2302
+ message: error.message,
2303
+ });
2304
+ // The supervisor gives up on a non-critical child whose start failed (a
2305
+ // critical one throws, and `startApp` says so). That went unseen while
2306
+ // the first child's start made the app `online`; now the app would wait
2307
+ // for a child that is not coming, in `starting` — which `omnitron start`
2308
+ // takes for a start in progress and leaves alone. A declared process
2309
+ // that did not start is an app that did not start.
2310
+ if (handle.status === 'starting')
2311
+ handle.markErrored();
2312
+ this.logger.error({ app: entry.name, child: childName, err: error.message }, 'Process failed to start');
2313
+ this.persistState();
2314
+ });
1435
2315
  supervisor.on('child:restart', (childName, attempt) => {
1436
2316
  handle.restarts = attempt;
1437
2317
  this.logger.info({ app: entry.name, child: childName, attempt }, 'Process restarting');
@@ -1458,9 +2338,45 @@ export class OrchestratorService extends EventEmitter {
1458
2338
  const workerHandle = this.pm.getWorkerHandle(processId);
1459
2339
  if (!workerHandle?.onLog)
1460
2340
  return;
1461
- workerHandle.onLog((line, _stream) => {
2341
+ workerHandle.onLog(this.childLogForwarder(appName, handle));
2342
+ }
2343
+ /**
2344
+ * Attach log capture to the workers of a topology POOL.
2345
+ *
2346
+ * `attachLogCapture` above is driven by `child:started`, which only
2347
+ * supervisor children emit — a pool worker is not one, so nothing ever
2348
+ * subscribed to its output. Everything those workers print went nowhere:
2349
+ * the OHLCV aggregator and the image transformer have been mute since they
2350
+ * were written, and since titan-pm started holding pre-capture output it was
2351
+ * also a megabyte per worker retained for a reader that never arrived.
2352
+ *
2353
+ * Attached once, at pool creation. A worker replaced later by autoscaling or
2354
+ * a health restart is not covered; these pools are fixed-size, and the
2355
+ * alternative is an event this package does not expose yet.
2356
+ */
2357
+ attachPoolLogCapture(appName, processName, handle, pool) {
2358
+ const forward = this.childLogForwarder(appName, handle);
2359
+ let attached = 0;
2360
+ for (const workerId of pool.getWorkerIds()) {
2361
+ const workerHandle = pool.getWorkerHandle(workerId);
2362
+ if (!workerHandle?.onLog)
2363
+ continue;
2364
+ workerHandle.onLog(forward);
2365
+ attached++;
2366
+ }
2367
+ this.logger.debug({ app: appName, process: processName, workers: attached }, 'Pool log capture attached');
2368
+ }
2369
+ /** The one place a child's line becomes an app's line. */
2370
+ childLogForwarder(appName, handle) {
2371
+ return (line, stream) => {
1462
2372
  // In-memory ring buffer (for live CLI tailing)
1463
2373
  handle.appendLog(line);
2374
+ // Separate stderr ring — feeds `lastExit.stderrTail` on death.
2375
+ // Without this, the operator running `omnitron inspect` after
2376
+ // a crash sees no hint at WHY (the in-flight pipe is gone).
2377
+ if (stream === 'stderr') {
2378
+ handle.appendStderr(line);
2379
+ }
1464
2380
  // Persist to disk via registered log handlers (LogManager)
1465
2381
  for (const handler of this.appLogHandlers) {
1466
2382
  try {
@@ -1470,6 +2386,108 @@ export class OrchestratorService extends EventEmitter {
1470
2386
  // Log handler failure must not break the app
1471
2387
  }
1472
2388
  }
2389
+ };
2390
+ }
2391
+ /**
2392
+ * Write the failure a child reported before it could start to the app's log
2393
+ * handlers, as one structured line.
2394
+ *
2395
+ * `error.childStack` is the stack from inside the child when it managed to
2396
+ * send one — for a module that throws on import that is the entire
2397
+ * diagnosis, and it exists nowhere else once the process is gone.
2398
+ */
2399
+ persistChildFailure(appName, childName, error) {
2400
+ const childStack = error.childStack;
2401
+ const entry = JSON.stringify({
2402
+ level: 50,
2403
+ time: new Date().toISOString(),
2404
+ processName: qualifyChildName(appName, childName),
2405
+ event: 'process.start_failed',
2406
+ err: { message: error.message, ...(childStack ? { stack: childStack } : {}) },
2407
+ msg: `Process failed to start: ${error.message}`,
2408
+ });
2409
+ for (const handler of this.appLogHandlers) {
2410
+ try {
2411
+ handler(appName, entry);
2412
+ }
2413
+ catch {
2414
+ // A log handler must not break failure reporting.
2415
+ }
2416
+ }
2417
+ }
2418
+ /**
2419
+ * Write a dead child's captured output to the app's log handlers.
2420
+ *
2421
+ * Emitted as pino-shaped JSON so it lands in the same file, at the same
2422
+ * level, as everything else that app ever logged — a reader tailing
2423
+ * `app.log` after a failed start finds the reason there rather than having
2424
+ * to know that this particular failure hides in a different place.
2425
+ */
2426
+ persistChildOutput(appName, childName, output, stream) {
2427
+ if (!output)
2428
+ return;
2429
+ const time = new Date().toISOString();
2430
+ for (const line of output.split('\n')) {
2431
+ if (!line.trim())
2432
+ continue;
2433
+ // Already-structured lines pass through untouched; the child may well
2434
+ // have got far enough to produce them.
2435
+ const entry = line.trimStart().startsWith('{')
2436
+ ? line
2437
+ : JSON.stringify({
2438
+ // stderr is not error. A boot script's own progress markers go
2439
+ // there, and a failing start emits a run of them — classifying
2440
+ // every one at 50 filled the log with errors that are nothing of
2441
+ // the sort, next to the single line that IS the failure. The
2442
+ // failure is reported at 50 by `persistChildFailure`; this is the
2443
+ // context around it, and `stream` keeps it greppable.
2444
+ level: stream === 'stderr' ? 40 : 30,
2445
+ time,
2446
+ processName: qualifyChildName(appName, childName),
2447
+ stream,
2448
+ msg: line,
2449
+ });
2450
+ for (const handler of this.appLogHandlers) {
2451
+ try {
2452
+ handler(appName, entry);
2453
+ }
2454
+ catch {
2455
+ // A log handler must not break failure reporting.
2456
+ }
2457
+ }
2458
+ }
2459
+ }
2460
+ /**
2461
+ * Subscribe to the WorkerHandle's `exit` event so the orchestrator
2462
+ * keeps the exit code / signal / "expected" flag past the
2463
+ * WorkerHandle's GC. Pre-fix the supervisor's `child:crash` event
2464
+ * fired with an Error but never surfaced the underlying
2465
+ * code/signal — operators had no way to tell SIGKILL from a
2466
+ * thrown TypeError. The captured info lands on
2467
+ * `handle.lastExit` and is surfaced by `omnitron inspect`.
2468
+ */
2469
+ attachExitCapture(appName, childName, handle) {
2470
+ const supervisor = handle.supervisor;
2471
+ if (!supervisor)
2472
+ return;
2473
+ const processId = supervisor.getChildProcessId(childName);
2474
+ if (!processId)
2475
+ return;
2476
+ const workerHandle = this.pm.getWorkerHandle(processId);
2477
+ if (!workerHandle?.onExit)
2478
+ return;
2479
+ workerHandle.onExit((info) => {
2480
+ // Preserve the supervisor's error message if `child:crash`
2481
+ // already fired before us. recordExit overwrites the synthetic
2482
+ // entry with the authoritative code/signal.
2483
+ const message = handle.lastExit?.message;
2484
+ handle.recordExit({
2485
+ code: info.code,
2486
+ signal: info.signal,
2487
+ expected: info.expected,
2488
+ ...(message !== undefined && { message }),
2489
+ });
2490
+ this.logger.debug({ app: appName, child: childName, code: info.code, signal: info.signal, expected: info.expected }, 'Captured worker exit context');
1473
2491
  });
1474
2492
  }
1475
2493
  // ============================================================================
@@ -1479,30 +2497,58 @@ export class OrchestratorService extends EventEmitter {
1479
2497
  const child = handle.childProcess;
1480
2498
  if (!child || !child.pid)
1481
2499
  return;
2500
+ const pid = child.pid;
2501
+ // P0-D — Signal the whole process group, not just the direct
2502
+ // child. classic-launcher spawns with `detached: true` so the
2503
+ // child becomes group leader (pgid === pid). `process.kill(-pid)`
2504
+ // delivers the signal to every process in the group, reaping
2505
+ // grandchildren (ffmpeg, docker, worker pools …) that ignore the
2506
+ // direct-child kill. Falls back to `child.kill` when the negative-
2507
+ // pid call fails (test environments, restricted permissions, the
2508
+ // child wasn't started detached — bootstrap-mode currently isn't).
2509
+ const sendSignal = (sig) => {
2510
+ try {
2511
+ process.kill(-pid, sig);
2512
+ return;
2513
+ }
2514
+ catch (err) {
2515
+ const code = err.code;
2516
+ if (code !== 'ESRCH' && code !== 'EPERM') {
2517
+ // ESRCH/EPERM = group already gone or not a group leader.
2518
+ // Anything else (EINVAL etc.) → log via best-effort.
2519
+ this.logger.debug({ app: handle.name, pid, sig, code }, 'process.kill(-pid) fallback');
2520
+ }
2521
+ }
2522
+ try {
2523
+ child.kill(sig);
2524
+ }
2525
+ catch { /* best-effort */ }
2526
+ };
1482
2527
  await new Promise((resolve) => {
1483
2528
  const timer = setTimeout(() => {
1484
- try {
1485
- child.kill('SIGKILL');
1486
- }
1487
- catch {
1488
- // SIGKILL send is best-effort
1489
- }
2529
+ sendSignal('SIGKILL');
1490
2530
  resolve();
1491
2531
  }, timeout);
1492
2532
  child.once('exit', () => {
1493
2533
  clearTimeout(timer);
1494
2534
  resolve();
1495
2535
  });
1496
- try {
1497
- child.kill(force ? 'SIGKILL' : 'SIGTERM');
1498
- }
1499
- catch {
1500
- clearTimeout(timer);
1501
- resolve();
1502
- }
2536
+ sendSignal(force ? 'SIGKILL' : 'SIGTERM');
1503
2537
  });
1504
2538
  }
1505
- async handleClassicCrash(entry, handle, config) {
2539
+ /**
2540
+ * Cancel a pending crash cool-down for `name`, if one is armed.
2541
+ * An explicit stop means "this app is going down now, stay down",
2542
+ * which the cool-down must not undo ten minutes later.
2543
+ */
2544
+ clearCoolDown(name) {
2545
+ const timer = this.coolDownTimers.get(name);
2546
+ if (timer) {
2547
+ clearTimeout(timer);
2548
+ this.coolDownTimers.delete(name);
2549
+ }
2550
+ }
2551
+ async scheduleRecovery(entry, handle, config) {
1506
2552
  const policy = buildRestartPolicy(entry, config);
1507
2553
  if (!policy.enabled)
1508
2554
  return;
@@ -1510,87 +2556,416 @@ export class OrchestratorService extends EventEmitter {
1510
2556
  handle.status = 'crashed';
1511
2557
  this.logger.error({ app: entry.name }, 'Max restarts exceeded');
1512
2558
  this.emit('app:escalated', entry.name);
2559
+ // P1-I — auto-recovery after a cool-down. pm2 has this as
2560
+ // `restart_delay` + a fresh attempt window; pre-fix omnitron
2561
+ // left the app permanently 'crashed' until human intervention.
2562
+ // We reset the counter + schedule one fresh attempt 10 minutes
2563
+ // out. If THAT attempt also crashes through maxRestarts we
2564
+ // land back here and pause another 10 minutes — bounded
2565
+ // self-healing without thrashing.
2566
+ //
2567
+ // The wait happens OUTSIDE the lifecycle queue, and that is the
2568
+ // whole point. It used to be a queued `crash-restart` whose
2569
+ // `run()` slept for the entire cool-down — but `enqueue` will
2570
+ // not preempt a RUNNING head, and with an empty queue the
2571
+ // sleeper always became the head immediately. So the `onPreempt`
2572
+ // hook meant to cancel it could never fire, and an operator's
2573
+ // `stop` or `restart` simply queued behind ten minutes of sleep:
2574
+ // the CLI's RPC timed out after 60s while the op stayed queued,
2575
+ // to fire whenever the cool-down finally elapsed. An app that
2576
+ // exhausted its restart budget was unreachable by every command
2577
+ // the CLI offers — `restart` hung, `stop` hung, `stack start`
2578
+ // skipped it — and the only recovery was restarting the daemon.
2579
+ const COOL_DOWN_MS = 10 * 60_000;
2580
+ this.clearCoolDown(entry.name);
2581
+ const timer = setTimeout(() => {
2582
+ this.coolDownTimers.delete(entry.name);
2583
+ // Re-read the handle: every start replaces it (see
2584
+ // `startAppInternal`), so the one captured above is stale the
2585
+ // moment anything restarts this app. Only a still-crashed app
2586
+ // wants this attempt — an operator who started it meanwhile
2587
+ // has already settled the question.
2588
+ const current = this.handles.get(entry.name);
2589
+ if (!current || current.status !== 'crashed')
2590
+ return;
2591
+ this.logger.info({ app: entry.name }, 'Cool-down elapsed — attempting fresh recovery');
2592
+ current.restarts = 0; // fresh attempt window
2593
+ current.status = 'stopped';
2594
+ void this.lifecycleQueue
2595
+ .enqueue(entry.name, 'crash-restart', () => this.startAppInternal(entry, config))
2596
+ .catch((err) => {
2597
+ if (err instanceof LifecyclePreempted)
2598
+ return;
2599
+ this.logger.error({ app: entry.name, err: err.message }, 'Cool-down recovery failed');
2600
+ });
2601
+ }, COOL_DOWN_MS);
2602
+ timer.unref?.();
2603
+ this.coolDownTimers.set(entry.name, timer);
1513
2604
  return;
1514
2605
  }
1515
2606
  const backoff = policy.backoff ?? config.supervision.backoff;
1516
2607
  const delay = this.calculateBackoff(handle.restarts, backoff);
1517
2608
  this.logger.info({ app: entry.name, delay, attempt: handle.restarts + 1 }, 'Scheduling restart');
1518
- const timer = setTimeout(async () => {
1519
- // Guard: don't restart if orchestrator was stopped during backoff
1520
- if (handle.status === 'stopping' || handle.status === 'stopped')
1521
- return;
1522
- try {
1523
- handle.status = 'stopped';
1524
- await this.startApp(entry, config);
1525
- }
1526
- catch (err) {
1527
- this.logger.error({ app: entry.name, error: err.message }, 'Restart failed');
2609
+ // P0-I — schedule the crash-restart through the lifecycle queue
2610
+ // so an explicit stop preempts the pending restart before it
2611
+ // fires. The setTimeout is still the timing source, but the
2612
+ // queue.enqueue with `onPreempt` guarantees the timer is cleared
2613
+ // (and the promise rejected with LifecyclePreempted) the moment a
2614
+ // higher-priority op for the same app arrives. Pre-fix the timer
2615
+ // could fire during stopApp's `await stopChild(...)` window and
2616
+ // spawn a zombie under the same handle.
2617
+ let timer = null;
2618
+ const restartPromise = this.lifecycleQueue.enqueue(entry.name, 'crash-restart', () => new Promise((resolve, reject) => {
2619
+ timer = setTimeout(async () => {
2620
+ timer = null;
2621
+ // Guard: don't restart if orchestrator was stopped during backoff
2622
+ if (handle.status === 'stopping' || handle.status === 'stopped') {
2623
+ resolve();
2624
+ return;
2625
+ }
2626
+ try {
2627
+ handle.status = 'stopped';
2628
+ // `startAppInternal`, not `startApp`.
2629
+ //
2630
+ // This task holds the lifecycle-queue slot for `entry.name`.
2631
+ // `startApp` enqueues 'start' on that same key, and the queue
2632
+ // cannot run it until this task returns — so the await never
2633
+ // settles and the restart silently never happens. Measured:
2634
+ // `startApp:enter` with no matching exit, and the app left in
2635
+ // whatever state the crash put it in.
2636
+ //
2637
+ // This is why the whole policy below — bounded attempts,
2638
+ // exponential backoff, the ten-minute cool-down described in its
2639
+ // own comment as "bounded self-healing" — had never restarted
2640
+ // anything. The queue was introduced (P0-I) to stop a restart
2641
+ // timer racing a stop; it also made the restart unreachable, and
2642
+ // nothing failed loudly enough to say so.
2643
+ await this.startAppInternal(entry, config);
2644
+ resolve();
2645
+ }
2646
+ catch (err) {
2647
+ const message = err.message;
2648
+ this.logger.error({ app: entry.name, error: message }, 'Restart failed');
2649
+ // Said separately and after, because reading the process table
2650
+ // takes a moment and the failure itself must not wait for it.
2651
+ void this.explainPortConflict(message).then((held) => {
2652
+ if (held)
2653
+ this.logger.error({ app: entry.name }, `Restart failed — ${held}`);
2654
+ });
2655
+ reject(err);
2656
+ }
2657
+ }, delay);
2658
+ }), () => {
2659
+ // onPreempt — explicit stop / restart arrived. Cancel the
2660
+ // pending timer so the restart never fires.
2661
+ if (timer) {
2662
+ clearTimeout(timer);
2663
+ timer = null;
1528
2664
  }
1529
- }, delay);
1530
- // Allow cleanup on explicit stop
2665
+ });
2666
+ // Swallow LifecyclePreempted — that's the explicit-stop happy
2667
+ // path, not an error. Anything else gets logged.
2668
+ restartPromise.catch((err) => {
2669
+ if (err instanceof LifecyclePreempted)
2670
+ return;
2671
+ this.logger.warn({ app: entry.name, err: err.message }, 'Crash restart task rejected');
2672
+ });
2673
+ // Legacy field — callers still poke handle.crashRestartTimer to
2674
+ // check "is a restart pending"; keep it in sync with the queued
2675
+ // timer so existing inspect / status code paths don't break.
1531
2676
  handle.crashRestartTimer = timer;
1532
2677
  }
1533
2678
  calculateBackoff(attempt, backoff) {
1534
- const initial = backoff.initial ?? 1000;
1535
- const max = backoff.max ?? 30_000;
1536
- const factor = backoff.factor ?? 2;
1537
- if (backoff.type === 'exponential')
1538
- return Math.min(initial * Math.pow(factor, attempt), max);
1539
- if (backoff.type === 'linear')
1540
- return Math.min(initial * (attempt + 1), max);
1541
- return initial;
1542
- }
1543
- /** Sample CPU/memory for a single OS process via `ps` (async — does not block event loop) */
2679
+ // Delegate to the shared backoff library in titan-pm. Pre-fix the
2680
+ // inline impl here defaulted `initial=1000` while supervisor's
2681
+ // copy defaulted to 300 — same config produced 3.3× longer
2682
+ // delays in classic-mode than bootstrap-mode. Now both routes
2683
+ // run the same math.
2684
+ return computeBackoff(attempt + 1, backoff);
2685
+ }
2686
+ /**
2687
+ * The live OS processes behind one topology entry.
2688
+ *
2689
+ * Single-instance entries run as supervisor children; entries with
2690
+ * `instances > 1` run in a pool the supervisor knows nothing about. Both
2691
+ * shapes are resolved here, in one place, because metrics sampling and
2692
+ * status reporting each used to resolve them separately — and disagreed.
2693
+ * The pool branch asserted `online` from `pool.size > 0` before looking at
2694
+ * a pid at all, so a worker whose process had exited still read as online
2695
+ * (observed live: two pool workers reporting `online` on pids that `kill
2696
+ * -0` said were gone).
2697
+ */
2698
+ resolveTopologyPids(handle, topo) {
2699
+ return this.resolveTopologyMembers(handle, topo).members.map((m) => m.pid);
2700
+ }
2701
+ /**
2702
+ * The live processes behind one topology entry, keyed by the PM process id
2703
+ * as well as the OS pid.
2704
+ *
2705
+ * The pid alone answers "is it alive"; the process id is what any question
2706
+ * ABOUT that process has to be asked with — `pm.getProcess(id)` carries the
2707
+ * spawn time, and the supervisor's restart counter is keyed by child name.
2708
+ * `resolveTopologyPids` used to discard both on the way out, which is why
2709
+ * the `omnitron list` DTO had nothing to report but the parent's numbers.
2710
+ */
2711
+ resolveTopologyMembers(handle, topo) {
2712
+ const isPoolEntry = (topo.instances ?? 1) > 1;
2713
+ if (isPoolEntry) {
2714
+ const pool = handle.topologyPools.get(topo.name);
2715
+ if (!pool || pool.size === 0)
2716
+ return { members: [], childName: null };
2717
+ const members = pool
2718
+ .getWorkerIds()
2719
+ .map((processId) => ({ processId, pid: this.pm.getWorkerHandle(processId)?.pid }))
2720
+ .filter((m) => typeof m.pid === 'number');
2721
+ return { members, childName: null };
2722
+ }
2723
+ const childNames = handle.supervisor?.getChildNames() ?? [];
2724
+ const childName = childNames.find((cn) => cn.includes(topo.name)) ?? topo.name;
2725
+ const processId = handle.supervisor?.getChildProcessId(childName);
2726
+ if (!processId)
2727
+ return { members: [], childName };
2728
+ const pid = this.pm.getWorkerHandle(processId)?.pid;
2729
+ return { members: typeof pid === 'number' ? [{ processId, pid }] : [], childName };
2730
+ }
2731
+ /**
2732
+ * Sample every process an app owns, and record the per-entry readings on
2733
+ * the handle on the way through.
2734
+ *
2735
+ * A bootstrap app is several OS processes, so "the memory this app uses"
2736
+ * is their sum. The periodic poller used to sample the single pid stored
2737
+ * on the handle — which for a multi-process app is whichever child was
2738
+ * registered last — and overwrite `lastMetrics` with it every five
2739
+ * seconds, while `getMetrics()` right beside it aggregated correctly and
2740
+ * wrote to the same field. The poller won on frequency, so the aggregate
2741
+ * was never what anyone saw: `acme/dev/main` reported 122 MB against 437 MB
2742
+ * actually resident across its three processes.
2743
+ *
2744
+ * ## Why `ps` and not the child's own report
2745
+ *
2746
+ * The aggregating path preferred the PM-level metrics a child reports over
2747
+ * its RPC channel, falling back to `ps`. Those two are not the same
2748
+ * quantities:
2749
+ *
2750
+ * - `cpu` from the child is `(cpuUsage().user + .system) / 1e6` — CPU
2751
+ * SECONDS CONSUMED SINCE START, a monotonically growing counter. `ps`
2752
+ * `%cpu` is a percentage. The dashboard column is labelled `CPU %`,
2753
+ * so an app running for a day would have read `86400`. Observed
2754
+ * climbing live: 12.845 to 13.297 across twelve seconds of idling.
2755
+ * - `memory` from the child is `memoryUsage().heapUsed` — the V8 heap.
2756
+ * `ps` reports RSS, which is what "memory this app uses" means to
2757
+ * whoever is sizing a host. Observed: 66 MB heap against 149 MB
2758
+ * resident for the same process.
2759
+ *
2760
+ * Mixing them per-child, depending on which source answered first, gave a
2761
+ * column that was neither quantity. `ps` is asked for both figures here so
2762
+ * that every number under one label means one thing.
2763
+ */
2764
+ async sampleAppMetrics(handle) {
2765
+ const topology = handle.topologyProcesses ?? [];
2766
+ const childNames = handle.supervisor?.getChildNames() ?? [];
2767
+ // Which pids belong to which topology entry. Everything is sampled in a
2768
+ // single `ps` call: sampling one child at a time would fork a process per
2769
+ // child per polling interval, and a monitor that costs more than what it
2770
+ // monitors is its own kind of defect.
2771
+ const pidsByEntry = new Map();
2772
+ for (const topo of topology) {
2773
+ pidsByEntry.set(topo.name, this.resolveTopologyPids(handle, topo));
2774
+ }
2775
+ if (topology.length === 0) {
2776
+ // No declared topology: fall back to the supervisor's children, and
2777
+ // failing that to the handle's own pid.
2778
+ for (const childName of childNames) {
2779
+ const processId = handle.supervisor?.getChildProcessId(childName);
2780
+ const pid = processId ? this.pm.getWorkerHandle(processId)?.pid : undefined;
2781
+ if (typeof pid === 'number')
2782
+ pidsByEntry.set(childName, [pid]);
2783
+ }
2784
+ if (pidsByEntry.size === 0 && handle.pid) {
2785
+ pidsByEntry.set(handle.name, [handle.pid]);
2786
+ }
2787
+ }
2788
+ const batch = await this.sampleProcessMetricsBatch([...pidsByEntry.values()].flat());
2789
+ let cpu = 0;
2790
+ let memory = 0;
2791
+ const sampled = new Map();
2792
+ for (const [name, pids] of pidsByEntry) {
2793
+ const sample = { cpu: 0, memory: 0 };
2794
+ for (const pid of pids) {
2795
+ const os = batch.get(pid);
2796
+ if (!os)
2797
+ continue;
2798
+ sample.cpu += os.cpu;
2799
+ sample.memory += os.memory;
2800
+ }
2801
+ sampled.set(name, sample);
2802
+ cpu += sample.cpu;
2803
+ memory += sample.memory;
2804
+ }
2805
+ // Traffic can only come from inside the child; `ps` knows nothing about
2806
+ // it. Its CPU and memory figures are deliberately NOT used — see the note
2807
+ // on this method. Only what a child reports as TRAFFIC is summed: the
2808
+ // `requests`/`errors` a child on an older runtime reports are the call
2809
+ // counters of its wrapper — the supervisor's own health and metrics
2810
+ // calls — and adding them up counted this daemon's questions as the
2811
+ // app's requests (`combineProcessTraffic`).
2812
+ const reports = [];
2813
+ for (const childName of childNames) {
2814
+ const reported = await handle.supervisor?.getChildMetrics(childName);
2815
+ reports.push(reported?.traffic);
2816
+ }
2817
+ const traffic = combineProcessTraffic(reports);
2818
+ // A sample that produced nothing for an app that HAS live processes is a
2819
+ // failed sample, not a measurement of zero — `ps` can time out under load,
2820
+ // and it did: an app was observed reporting 0 MB one second and 143 MB the
2821
+ // next. Overwriting the last known reading with zeros would put the exact
2822
+ // placeholder this method exists to remove back on the dashboard, so the
2823
+ // previous reading is kept instead.
2824
+ const sampledNothing = batch.size === 0 && [...pidsByEntry.values()].some((pids) => pids.length > 0);
2825
+ if (sampledNothing && handle.lastMetrics) {
2826
+ return handle.lastMetrics;
2827
+ }
2828
+ handle.childMetrics = sampled;
2829
+ return traffic
2830
+ ? {
2831
+ cpu,
2832
+ memory,
2833
+ requests: traffic.requests,
2834
+ errors: traffic.serverErrors,
2835
+ traffic,
2836
+ ...(traffic.latency
2837
+ ? {
2838
+ latency: {
2839
+ p50: traffic.latency.p50,
2840
+ p75: traffic.latency.p75,
2841
+ p90: traffic.latency.p90,
2842
+ p95: traffic.latency.p95,
2843
+ p99: traffic.latency.p99,
2844
+ mean: traffic.latency.mean,
2845
+ },
2846
+ }
2847
+ : {}),
2848
+ }
2849
+ : { cpu, memory };
2850
+ }
2851
+ /**
2852
+ * Sample several processes in one `ps` call.
2853
+ *
2854
+ * Pids the OS no longer knows are simply absent from the result — an
2855
+ * omission the caller can see, unlike a zeroed reading it cannot tell
2856
+ * apart from an idle process.
2857
+ */
2858
+ async sampleProcessMetricsBatch(pids) {
2859
+ const out = new Map();
2860
+ const unique = [...new Set(pids)];
2861
+ if (unique.length === 0)
2862
+ return out;
2863
+ try {
2864
+ const psOutput = await new Promise((resolve, reject) => {
2865
+ execFile('ps', ['-p', unique.join(','), '-o', 'pid=,rss=,%cpu='], { timeout: 5000 }, (err, stdout) => {
2866
+ // `ps` exits non-zero when NONE of the pids exist, but still
2867
+ // prints the ones that do — so stdout is worth reading either way.
2868
+ if (err && !stdout)
2869
+ reject(err);
2870
+ else
2871
+ resolve(stdout.trim());
2872
+ });
2873
+ });
2874
+ for (const [pid, sample] of parsePsBatch(psOutput))
2875
+ out.set(pid, sample);
2876
+ }
2877
+ catch {
2878
+ // Leave the map empty: no reading is honest, a zero reading is not.
2879
+ }
2880
+ return out;
2881
+ }
2882
+ /**
2883
+ * Sample CPU/memory for a single OS process via `ps`.
2884
+ *
2885
+ * `null` when it could not be read. This used to return
2886
+ * `{ cpu: 0, memory: 0 }` on any failure, and the caller wrote that
2887
+ * straight over `handle.lastMetrics` — so a `ps` that timed out under load
2888
+ * made a running classic-mode app read as idle, indistinguishable from one
2889
+ * that genuinely was. The batch path above already treated absence as
2890
+ * absence; these two now agree.
2891
+ */
1544
2892
  async sampleProcessMetrics(pid) {
1545
2893
  try {
1546
- const { execFile } = await import('node:child_process');
1547
2894
  const psOutput = await new Promise((resolve, reject) => {
1548
- execFile('ps', ['-p', String(pid), '-o', 'rss=,%cpu='], { timeout: 5000 }, (err, stdout) => {
1549
- if (err)
2895
+ execFile('ps', ['-p', String(pid), '-o', 'pid=,rss=,%cpu='], { timeout: 5000 }, (err, stdout) => {
2896
+ if (err && !stdout)
1550
2897
  reject(err);
1551
2898
  else
1552
2899
  resolve(stdout.trim());
1553
2900
  });
1554
2901
  });
1555
- const [rssKb, cpuPercent] = psOutput.split(/\s+/);
1556
- return {
1557
- cpu: parseFloat(cpuPercent ?? '0'),
1558
- memory: parseInt(rssKb ?? '0', 10) * 1024,
1559
- };
2902
+ return parsePsLine(psOutput)?.sample ?? null;
1560
2903
  }
1561
2904
  catch {
1562
- return { cpu: 0, memory: 0 };
2905
+ return null;
1563
2906
  }
1564
2907
  }
1565
- /** Periodic metrics polling for classic mode processes */
2908
+ /**
2909
+ * Periodic CPU/memory sampling for every managed process.
2910
+ *
2911
+ * Idempotent: re-arming replaces the previous timer rather than stacking a
2912
+ * second one, so `setConfig` can call it on every config reload.
2913
+ */
1566
2914
  startMetricsPolling(interval) {
2915
+ if (this.metricsTimer) {
2916
+ clearInterval(this.metricsTimer);
2917
+ this.metricsTimer = null;
2918
+ }
1567
2919
  this.metricsTimer = setInterval(async () => {
1568
2920
  for (const handle of this.handles.values()) {
1569
2921
  if (handle.status !== 'online')
1570
2922
  continue;
1571
2923
  if (handle.mode === 'classic' && handle.childProcess?.pid) {
1572
2924
  const m = await this.sampleProcessMetrics(handle.childProcess.pid);
1573
- handle.lastMetrics = { ...m, requests: 0, errors: 0 };
2925
+ // A failed read keeps the previous numbers rather than replacing
2926
+ // them with zeros — the same rule the bootstrap path below uses.
2927
+ if (m) {
2928
+ handle.lastMetrics = {
2929
+ ...m,
2930
+ requests: handle.lastMetrics?.requests ?? 0,
2931
+ errors: handle.lastMetrics?.errors ?? 0,
2932
+ };
2933
+ }
1574
2934
  }
1575
- else if (handle.mode === 'bootstrap' && handle.pid) {
1576
- const m = await this.sampleProcessMetrics(handle.pid);
1577
- handle.lastMetrics = {
1578
- ...m,
1579
- requests: handle.lastMetrics?.requests ?? 0,
1580
- errors: handle.lastMetrics?.errors ?? 0,
1581
- };
2935
+ else if (handle.mode === 'bootstrap') {
2936
+ // Every process the app owns, not just the one on the handle.
2937
+ // As sampled. The counts used to fall back to the previous reading
2938
+ // whenever a sample said 0 — which kept a stale figure alive
2939
+ // across a process restart (traffic counts restart from zero with
2940
+ // the process) and, while `requests` was the wrapper's call count,
2941
+ // mixed the two meanings in one field.
2942
+ handle.lastMetrics = await this.sampleAppMetrics(handle);
1582
2943
  }
1583
2944
  }
1584
2945
  }, interval);
2946
+ this.metricsTimer.unref();
1585
2947
  }
1586
2948
  // ============================================================================
1587
2949
  // Private — Utilities
1588
2950
  // ============================================================================
1589
2951
  toProcessInfo(handle) {
2952
+ // Ghost-online guard. The in-memory `handle.status` reflects the
2953
+ // last event the supervisor observed. If a child died between
2954
+ // the last sweep and now (kill -9 by an operator, OOM kill,
2955
+ // container restart) the handle still claims 'online' until the
2956
+ // next event lands. A cheap signal-0 verification at DTO-mapping
2957
+ // time keeps `omnitron list` honest: when the OS reports the pid
2958
+ // is gone, surface 'crashed' instead of lying to the operator.
2959
+ // `isAlive` covers EPERM as alive (correct — process exists, we
2960
+ // just can't signal it).
2961
+ let status = handle.status;
2962
+ if (status === 'online' && handle.pid != null && !isAlive(handle.pid)) {
2963
+ status = 'crashed';
2964
+ }
1590
2965
  const info = {
1591
2966
  name: handle.name,
1592
2967
  pid: handle.pid,
1593
- status: handle.status,
2968
+ status,
1594
2969
  cpu: handle.cpu,
1595
2970
  memory: handle.memory,
1596
2971
  uptime: handle.uptime,
@@ -1605,49 +2980,48 @@ export class OrchestratorService extends EventEmitter {
1605
2980
  info.processes = [];
1606
2981
  for (const topo of handle.topologyProcesses) {
1607
2982
  const isPoolEntry = (topo.instances ?? 1) > 1;
1608
- let childPid = null;
1609
- let childStatus = 'stopped';
1610
- if (isPoolEntry) {
1611
- // Pool-managed topology — the supervisor doesn't know about
1612
- // these (workers are owned by ProcessPool). Read directly
1613
- // from the pool ref we stashed at creation time.
1614
- const pool = handle.topologyPools.get(topo.name);
1615
- if (pool && pool.size > 0) {
1616
- childStatus = 'online';
1617
- // Surface the first worker's PID. Pool workers are
1618
- // interchangeable so any one is representative; the CLI
1619
- // prints "PID" as a quick liveness signal, not a unique
1620
- // identifier.
1621
- const workerIds = pool.getWorkerIds();
1622
- if (workerIds.length > 0) {
1623
- const firstId = workerIds[0];
1624
- const wh = this.pm.getWorkerHandle(firstId);
1625
- childPid = wh?.pid ?? null;
1626
- }
1627
- }
1628
- }
1629
- else {
1630
- // Single-instance topology — supervisor manages the child directly.
1631
- const childNames = handle.supervisor.getChildNames();
1632
- const childName = childNames.find((cn) => cn.includes(topo.name)) ?? topo.name;
1633
- const processId = handle.supervisor.getChildProcessId(childName);
1634
- if (processId) {
1635
- const wh = this.pm.getWorkerHandle(processId);
1636
- childPid = wh?.pid ?? null;
1637
- childStatus = childPid ? 'online' : 'stopped';
1638
- }
1639
- }
2983
+ // The same ghost-online guard the parent gets a few lines above.
2984
+ // Without it the pool branch reported `online` from the pool's own
2985
+ // bookkeeping, never checking whether the pid it printed alongside
2986
+ // still existed.
2987
+ const { members, childName } = this.resolveTopologyMembers(handle, topo);
2988
+ const alive = members.filter((m) => isAlive(m.pid));
2989
+ const declaredInstances = topo.instances ?? 1;
2990
+ const liveInstances = isPoolEntry
2991
+ ? (handle.topologyPools.get(topo.name)?.size ?? 0)
2992
+ : alive.length;
2993
+ const chosen = alive[0] ?? members[0];
2994
+ const childPid = chosen?.pid ?? null;
2995
+ const childStatus = alive.length > 0 ? 'online' : members.length > 0 ? 'crashed' : 'stopped';
2996
+ // The row prints one pid, so the row's clock is that pid's clock.
2997
+ // `startTime` is written when the process id is registered and a
2998
+ // restart registers a new one, so it moves when the process does.
2999
+ // (`IProcessInfo.restartCount` does NOT: it is assigned 0 there and
3000
+ // incremented nowhere. The supervisor keeps the real count, by child
3001
+ // name — reading the field instead would have replaced a wrong
3002
+ // constant with a wrong lookup.)
3003
+ const chosenInfo = chosen ? this.pm.getProcess(chosen.processId) : undefined;
3004
+ const childUptime = typeof chosenInfo?.startTime === 'number' && chosenInfo.startTime > 0
3005
+ ? Math.max(0, Date.now() - chosenInfo.startTime)
3006
+ : 0;
3007
+ // A pool is not a supervisor child and nothing counts its restarts.
3008
+ // `null` says so; `0` would be absence written as a value from the
3009
+ // domain, leaving "never restarted" and "not tracked" identical.
3010
+ const childRestarts = isPoolEntry || !childName ? null : (handle.supervisor?.getRestartCount(childName) ?? null);
1640
3011
  // Derive process type from declarations for backward-compatible DTO
1641
3012
  const derivedType = topo.transports ? 'server' : isPoolEntry ? 'worker' : 'custom';
3013
+ const sample = handle.childMetrics.get(topo.name);
1642
3014
  info.processes.push({
1643
3015
  name: topo.name,
1644
3016
  type: derivedType,
1645
3017
  pid: childPid,
1646
3018
  status: childStatus,
1647
- cpu: 0,
1648
- memory: 0,
1649
- uptime: handle.uptime,
1650
- restarts: 0,
3019
+ cpu: sample?.cpu ?? 0,
3020
+ memory: sample?.memory ?? 0,
3021
+ uptime: childUptime,
3022
+ restarts: childRestarts,
3023
+ instances: liveInstances,
3024
+ declaredInstances,
1651
3025
  });
1652
3026
  }
1653
3027
  }