cartethyia 2.7.0 → 2.7.2

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 (330) hide show
  1. package/.env.example +107 -106
  2. package/README.md +25 -2
  3. package/dist/dashboard/assets/{BannedPage-P76QLGJA.js → BannedPage-XaUmiBrC.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-XaUmiBrC.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-DdBggJS9.js → CliToolDetailPage-CqD7QiLM.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-aN1iXhhP.js → CliToolsPage-0XUo4UMn.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js.gz +0 -0
  9. package/dist/dashboard/assets/{CombosPage-DE-F1nxZ.js → CombosPage-CXGS_Ct-.js} +1 -1
  10. package/dist/dashboard/assets/CombosPage-CXGS_Ct-.js.gz +0 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js +1 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-3CUxMy2X.js → CustomizationPage-DAG5J9EY.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js.gz +0 -0
  17. package/dist/dashboard/assets/LoginPage-BdmRLnq2.js +1 -0
  18. package/dist/dashboard/assets/LoginPage-BdmRLnq2.js.gz +0 -0
  19. package/dist/dashboard/assets/{ModelPicker-DKVslqG3.js → ModelPicker-BH8Tehdi.js} +1 -1
  20. package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-CqMxibJB.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-CqMxibJB.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js +4 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +1 -0
  26. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
  27. package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js +1 -0
  28. package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js.gz +0 -0
  29. package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js +9 -0
  30. package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js.gz +0 -0
  31. package/dist/dashboard/assets/{QuotaPage-BjldQwIU.js → QuotaPage-7P2n--Fv.js} +1 -1
  32. package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js.gz +0 -0
  33. package/dist/dashboard/assets/{SettingsPage-DUIoLRxk.js → SettingsPage-CxZ1fTkN.js} +1 -1
  34. package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js.gz +0 -0
  35. package/dist/dashboard/assets/SetupPage-Co12Zzaw.js +1 -0
  36. package/dist/dashboard/assets/SetupPage-Co12Zzaw.js.gz +0 -0
  37. package/dist/dashboard/assets/{SortableList-fI7JkdIB.js → SortableList-Dqf4zpS-.js} +1 -1
  38. package/dist/dashboard/assets/SortableList-Dqf4zpS-.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-CHVZqskZ.js → StudioPage-Byj2lAEm.js} +3 -3
  40. package/dist/dashboard/assets/StudioPage-Byj2lAEm.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-CTkLAuRc.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-CTkLAuRc.js.gz +0 -0
  43. package/dist/dashboard/assets/{api-keys-DFY_fZU4.js → api-keys-CUi6UXsR.js} +1 -1
  44. package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
  45. package/dist/dashboard/assets/app-KvsUJUMF.css +2 -0
  46. package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-CwdnZARt.js → assert-67xXH5rf.js} +1 -1
  48. package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-Dr-uf0Au.js → badge-DC2L43Mj.js} +1 -1
  50. package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
  51. package/dist/dashboard/assets/{banner-BddmMdoS.js → banner-BBVcb0J3.js} +1 -1
  52. package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
  53. package/dist/dashboard/assets/{card-CK0PXWlX.js → card-Cca1XC6G.js} +1 -1
  54. package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
  55. package/dist/dashboard/assets/{cli-tools-CcB6mGh2.js → cli-tools-KvsWa1Jl.js} +1 -1
  56. package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
  57. package/dist/dashboard/assets/{clipboard-button-DGb2P9Yx.js → clipboard-button-DUzEbynq.js} +1 -1
  58. package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
  59. package/dist/dashboard/assets/{dialog-LkfuL50u.js → dialog-DFvWhwTa.js} +1 -1
  60. package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
  61. package/dist/dashboard/assets/{drawer-ZbEZ_84c.js → drawer-CFM2sMX1.js} +1 -1
  62. package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
  63. package/dist/dashboard/assets/{entry-BJfsVMvA.js → entry-Bqy8RFvM.js} +1 -1
  64. package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
  65. package/dist/dashboard/assets/{entry-CWBY5A0f.js → entry-ClCMCWWQ.js} +3 -3
  66. package/dist/dashboard/assets/entry-ClCMCWWQ.js.gz +0 -0
  67. package/dist/dashboard/assets/{entry-CvC1-Zqx.js → entry-_xHo5x6r.js} +1 -1
  68. package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
  69. package/dist/dashboard/assets/formatters-CWWZ-uCZ.js +1 -0
  70. package/dist/dashboard/assets/formatters-CWWZ-uCZ.js.gz +0 -0
  71. package/dist/dashboard/assets/{github-badge-BxpznFLt.js → github-badge-C9e8Ts7e.js} +1 -1
  72. package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
  73. package/dist/dashboard/assets/{index-CDzP3ovJ.js → index-DFVFrnWW.js} +2 -2
  74. package/dist/dashboard/assets/index-DFVFrnWW.js.gz +0 -0
  75. package/dist/dashboard/assets/{inline-Ae13MQ5w.js → inline-BFUcvllj.js} +1 -1
  76. package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
  77. package/dist/dashboard/assets/{input-D23fo7cp.js → input-C_ILAvBb.js} +1 -1
  78. package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
  79. package/dist/dashboard/assets/{layout-CSsxjj5P.js → layout-CBIyT6Rt.js} +1 -1
  80. package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
  81. package/dist/dashboard/assets/{list-row-dmbwZMDY.js → list-row-ebJ_-Nsb.js} +1 -1
  82. package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
  83. package/dist/dashboard/assets/{live-BG1oUsZZ.js → live-D0mm--ME.js} +1 -1
  84. package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
  85. package/dist/dashboard/assets/{mutation-DSF-e4ho.js → mutation-sMD965oO.js} +1 -1
  86. package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
  87. package/dist/dashboard/assets/{numbers-Bm_aECY7.js → numbers-BO9dKaFr.js} +1 -1
  88. package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
  89. package/dist/dashboard/assets/providers/inception.svg +5 -0
  90. package/dist/dashboard/assets/providers/inception.svg.gz +0 -0
  91. package/dist/dashboard/assets/{query-keys-CF56nYIV.js → query-keys-CfL_Zdoh.js} +1 -1
  92. package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
  93. package/dist/dashboard/assets/{routing-CeA71n7G.js → routing-BnIpi1IJ.js} +1 -1
  94. package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
  95. package/dist/dashboard/assets/{select-KIT0rC6G.js → select-DBNVLdUu.js} +1 -1
  96. package/dist/dashboard/assets/select-DBNVLdUu.js.gz +0 -0
  97. package/dist/dashboard/assets/{settings-B_2gkePf.js → settings-BAFhDgX8.js} +1 -1
  98. package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
  99. package/dist/dashboard/assets/{stack-D7WOSzcX.js → stack-EMrOMt4I.js} +1 -1
  100. package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
  101. package/dist/dashboard/assets/{state-BBxtKZOQ.js → state-CWoo7ty5.js} +1 -1
  102. package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
  103. package/dist/dashboard/assets/{switch-DNS_D5Ea.js → switch-CCjT6IkF.js} +1 -1
  104. package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
  105. package/dist/dashboard/assets/{useMutation-BgoxdoMk.js → useMutation-DjkLHUcE.js} +1 -1
  106. package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
  107. package/dist/dashboard/index.html +1 -1
  108. package/dist/dashboard/index.html.gz +0 -0
  109. package/dist/dashboard/precompressed-assets.json +54 -53
  110. package/migrations/full-postgres/0024_add_pool_provider_cooldowns.sql +12 -0
  111. package/migrations/full-postgres/0025_add_pool_random_strategy.sql +3 -0
  112. package/migrations/lite-sqlite3/0024_add_pool_provider_cooldowns.sql +12 -0
  113. package/migrations/lite-sqlite3/0025_add_pool_random_strategy.sql +20 -0
  114. package/package.json +96 -96
  115. package/scripts/cli/console-menu.ts +5 -2
  116. package/scripts/cli/tray.ts +21 -6
  117. package/src/boot/app.ts +684 -714
  118. package/src/boot/dependencies.ts +65 -62
  119. package/src/config/cooldowns.ts +16 -0
  120. package/src/config/providers.ts +13 -1
  121. package/src/config/resources.ts +70 -70
  122. package/src/config/security.ts +37 -37
  123. package/src/config/spec.ts +371 -358
  124. package/src/console/cli-tools/contracts.ts +10 -4
  125. package/src/console/console-router.ts +233 -225
  126. package/src/console/domain-registration.ts +398 -381
  127. package/src/console/providers/catalog/catalog-projections.ts +2 -6
  128. package/src/console/providers/catalog/contracts.ts +19 -0
  129. package/src/console/providers/catalog/provider-operations.ts +38 -0
  130. package/src/console/providers/catalog/routes.ts +14 -0
  131. package/src/console/providers/catalog/store-helpers.ts +326 -0
  132. package/src/console/providers/catalog/store.ts +1292 -1563
  133. package/src/console/quota/account-quota-global.ts +2 -2
  134. package/src/console/quota/account-quota-shared.ts +1 -1
  135. package/src/console/quota/account-quota-tenant.ts +35 -12
  136. package/src/console/quota/quota-view.ts +2 -2
  137. package/src/console/routing/pools/contracts.ts +41 -1
  138. package/src/console/routing/pools/operations.ts +183 -4
  139. package/src/console/routing/pools/relay-deploy.ts +17 -4
  140. package/src/console/routing/pools/routes.ts +30 -0
  141. package/src/console/routing/pools/store.ts +98 -8
  142. package/src/main.ts +183 -167
  143. package/src/network/outbound-fetch.ts +124 -208
  144. package/src/network/pool/agent.ts +15 -0
  145. package/src/network/pool/pool-state.ts +7 -0
  146. package/src/network/pool/selector.ts +110 -2
  147. package/src/network/pool-cooldown-store.ts +252 -0
  148. package/src/network/pool-health-machine.ts +5 -3
  149. package/src/network/pool-health.ts +36 -9
  150. package/src/observability/event-query.ts +57 -0
  151. package/src/observability/metrics.ts +467 -467
  152. package/src/observability/runtime-metrics.ts +269 -269
  153. package/src/persistence/health-event-flush.ts +2 -0
  154. package/src/persistence/schema.ts +19 -0
  155. package/src/persistence/usage-flush.ts +8 -3
  156. package/src/protocol/canonical-model.ts +1 -1
  157. package/src/protocol/dsml.ts +377 -0
  158. package/src/protocol/gateway-error.ts +232 -232
  159. package/src/{providers/provider-reasoning.ts → protocol/reasoning.ts} +10 -9
  160. package/src/protocol/request/chat.ts +7 -7
  161. package/src/protocol/request/codex.ts +2 -2
  162. package/src/protocol/request/gemini.ts +1 -1
  163. package/src/protocol/request/messages.ts +6 -6
  164. package/src/protocol/request/responses.ts +7 -7
  165. package/src/protocol/response/dsml.ts +12 -133
  166. package/src/{transport → protocol}/translation/attachment-integrity.ts +2 -2
  167. package/src/{transport → protocol}/translation/cache-controls.ts +1 -1
  168. package/src/{transport → protocol}/translation/capabilities.ts +2 -2
  169. package/src/{transport → protocol}/translation/normalize.ts +1 -1
  170. package/src/{transport → protocol}/translation/quirks.ts +1 -1
  171. package/src/{transport → protocol}/translation/search.ts +1 -1
  172. package/src/{transport → protocol}/translation/thinking.ts +23 -1
  173. package/src/{transport → protocol}/translation/tool-id.ts +1 -1
  174. package/src/{transport → protocol}/translation/tool-repair.ts +2 -2
  175. package/src/protocol/upstream-parsing.ts +176 -0
  176. package/src/providers/adapters/compatible-adapter.ts +11 -2
  177. package/src/providers/authentication/oauth-refresh-service.ts +9 -5
  178. package/src/providers/default-registry.ts +22 -5
  179. package/src/providers/discovery/probe-phases.ts +16 -3
  180. package/src/providers/discovery/probing-service.ts +4 -1
  181. package/src/providers/integrations/anthropic.ts +1 -1
  182. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -4
  183. package/src/providers/integrations/buddy/buddy-chat-shared.ts +141 -8
  184. package/src/providers/integrations/buddy/buddy-checkin.ts +5 -1
  185. package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -82
  186. package/src/providers/integrations/buddy/buddy-identity.ts +25 -0
  187. package/src/providers/integrations/buddy/buddy-server-tools.ts +240 -0
  188. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +36 -0
  189. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +6 -31
  190. package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -0
  191. package/src/providers/integrations/buddy/codebuddy/shared.ts +19 -4
  192. package/src/providers/integrations/buddy/turing-device-token.ts +262 -0
  193. package/src/providers/integrations/buddy/workbuddy/provider.ts +2 -0
  194. package/src/providers/integrations/buddy/workbuddy/shared.ts +56 -3
  195. package/src/providers/integrations/claude/claude.ts +1 -1
  196. package/src/providers/integrations/cloudflareai/cloudflareai.ts +1 -1
  197. package/src/providers/integrations/codex/codex-errors.ts +1 -1
  198. package/src/providers/integrations/codex/codex.ts +1 -1
  199. package/src/providers/integrations/factory/factory.ts +1 -1
  200. package/src/providers/integrations/github/github.ts +1 -1
  201. package/src/providers/integrations/grok/grok.ts +1 -1
  202. package/src/providers/integrations/inception/provider.ts +91 -0
  203. package/src/providers/integrations/inferhub.ts +1 -1
  204. package/src/providers/integrations/kiro/kiro-request.ts +1 -1
  205. package/src/providers/integrations/mistral.ts +193 -19
  206. package/src/providers/integrations/ollamacloud-models.ts +50 -0
  207. package/src/providers/integrations/opencode/fingerprint.ts +19 -2
  208. package/src/providers/integrations/opencode/prompt.ts +111 -0
  209. package/src/providers/integrations/opencode/provider.ts +101 -23
  210. package/src/providers/integrations/opencode/tool-definitions.ts +4 -4
  211. package/src/providers/integrations/qoder/qoder-checkin.ts +303 -0
  212. package/src/providers/integrations/qoder/qoder-oauth.ts +12 -1
  213. package/src/providers/integrations/qoder/qoder-realm.ts +20 -0
  214. package/src/providers/integrations/qoder/qoder-text-tools.ts +277 -0
  215. package/src/providers/integrations/qoder/qoder.ts +315 -400
  216. package/src/providers/integrations/shared/install-id.ts +2 -1
  217. package/src/providers/integrations/trae/trae-cache.ts +110 -0
  218. package/src/providers/integrations/trae/trae-oauth.ts +29 -4
  219. package/src/providers/integrations/trae/trae-protocol.ts +2 -1
  220. package/src/providers/integrations/trae/trae.ts +106 -23
  221. package/src/providers/operations/account-error-classification.ts +1684 -0
  222. package/src/providers/operations/account-health-service.ts +27 -1503
  223. package/src/providers/operations/account-reset-service.ts +2 -0
  224. package/src/providers/operations/provider-catalog-service.ts +11 -1
  225. package/src/providers/provider-contracts.ts +7 -4
  226. package/src/providers/provider-metadata.ts +25 -4
  227. package/src/providers/provider-probe.ts +2 -2
  228. package/src/{console → providers}/quota/cache.ts +1 -1
  229. package/src/{console → providers}/quota/refresh.ts +5 -5
  230. package/src/security/cli-client-fingerprint.ts +9 -2
  231. package/src/transport/dispatch/attempt-finalize.ts +114 -12
  232. package/src/transport/dispatch/attempt-loop.ts +3 -0
  233. package/src/transport/dispatch/empty-stream.ts +9 -2
  234. package/src/transport/dispatch/generation.ts +37 -55
  235. package/src/transport/dispatch/handler-deps.ts +32 -0
  236. package/src/transport/dispatch/leases.ts +9 -4
  237. package/src/transport/dispatch/proxy-request.ts +6 -30
  238. package/src/transport/dispatch/responses-compact.ts +22 -43
  239. package/src/transport/dispatch/streaming/finalize.ts +3 -0
  240. package/src/transport/dispatch/streaming/stream.ts +0 -8
  241. package/src/transport/dispatch/systemone.ts +22 -42
  242. package/src/transport/dispatch/token-count.ts +23 -39
  243. package/src/transport/dispatch/upstream.ts +1 -3
  244. package/src/transport/dispatch/websearch.ts +22 -41
  245. package/src/transport/failure-policy.ts +92 -178
  246. package/src/transport/guards/abuse-guard.ts +10 -0
  247. package/src/transport/guards/auth-guard.ts +109 -109
  248. package/src/transport/guards/error-lifecycle.ts +1 -1
  249. package/src/transport/guards/pipeline.ts +220 -220
  250. package/src/transport/guards/request-context.ts +245 -238
  251. package/src/transport/request/degradation.ts +20 -1
  252. package/src/transport/request/inflight.ts +4 -114
  253. package/src/transport/request/preparer.ts +6 -6
  254. package/src/transport/request/state.ts +66 -38
  255. package/src/transport/request/tenant-preferences.ts +1 -1
  256. package/src/transport/routing/route-catalog.ts +75 -43
  257. package/src/transport/routing/router.ts +30 -2
  258. package/src/transport/surface/messages/parse.ts +1 -1
  259. package/src/transport/surface/responses/encode.ts +2 -3
  260. package/src/transport/surface/responses/parse.ts +49 -7
  261. package/src/types/undici.d.ts +33 -0
  262. package/src/workers/buddy-growth-tasks.ts +346 -425
  263. package/src/workers/daily-checkin.ts +17 -6
  264. package/src/workers/quota-refresh-sweep.ts +2 -2
  265. package/src/workers/scheduler.ts +17 -1
  266. package/dist/dashboard/assets/BannedPage-P76QLGJA.js.gz +0 -0
  267. package/dist/dashboard/assets/CliToolDetailPage-DdBggJS9.js.gz +0 -0
  268. package/dist/dashboard/assets/CliToolsPage-aN1iXhhP.js.gz +0 -0
  269. package/dist/dashboard/assets/CombosPage-DE-F1nxZ.js.gz +0 -0
  270. package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js +0 -1
  271. package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js.gz +0 -0
  272. package/dist/dashboard/assets/CustomizationPage-3CUxMy2X.js.gz +0 -0
  273. package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js +0 -1
  274. package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js.gz +0 -0
  275. package/dist/dashboard/assets/LoginPage-B_0FUKWc.js +0 -1
  276. package/dist/dashboard/assets/LoginPage-B_0FUKWc.js.gz +0 -0
  277. package/dist/dashboard/assets/ModelPicker-DKVslqG3.js.gz +0 -0
  278. package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js +0 -1
  279. package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js.gz +0 -0
  280. package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js +0 -3
  281. package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js.gz +0 -0
  282. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +0 -1
  283. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
  284. package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js +0 -1
  285. package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js.gz +0 -0
  286. package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js +0 -7
  287. package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js.gz +0 -0
  288. package/dist/dashboard/assets/QuotaPage-BjldQwIU.js.gz +0 -0
  289. package/dist/dashboard/assets/SettingsPage-DUIoLRxk.js.gz +0 -0
  290. package/dist/dashboard/assets/SetupPage-_bCaTM0v.js +0 -1
  291. package/dist/dashboard/assets/SetupPage-_bCaTM0v.js.gz +0 -0
  292. package/dist/dashboard/assets/SortableList-fI7JkdIB.js.gz +0 -0
  293. package/dist/dashboard/assets/StudioPage-CHVZqskZ.js.gz +0 -0
  294. package/dist/dashboard/assets/UsagePage-DiFBymHj.js +0 -56
  295. package/dist/dashboard/assets/UsagePage-DiFBymHj.js.gz +0 -0
  296. package/dist/dashboard/assets/api-keys-DFY_fZU4.js.gz +0 -0
  297. package/dist/dashboard/assets/app-BOJ1_xUB.css +0 -2
  298. package/dist/dashboard/assets/app-BOJ1_xUB.css.gz +0 -0
  299. package/dist/dashboard/assets/assert-CwdnZARt.js.gz +0 -0
  300. package/dist/dashboard/assets/badge-Dr-uf0Au.js.gz +0 -0
  301. package/dist/dashboard/assets/banner-BddmMdoS.js.gz +0 -0
  302. package/dist/dashboard/assets/card-CK0PXWlX.js.gz +0 -0
  303. package/dist/dashboard/assets/cli-tools-CcB6mGh2.js.gz +0 -0
  304. package/dist/dashboard/assets/clipboard-button-DGb2P9Yx.js.gz +0 -0
  305. package/dist/dashboard/assets/cog-CdWhsajs.js +0 -1
  306. package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
  307. package/dist/dashboard/assets/dialog-LkfuL50u.js.gz +0 -0
  308. package/dist/dashboard/assets/drawer-ZbEZ_84c.js.gz +0 -0
  309. package/dist/dashboard/assets/entry-BJfsVMvA.js.gz +0 -0
  310. package/dist/dashboard/assets/entry-CWBY5A0f.js.gz +0 -0
  311. package/dist/dashboard/assets/entry-CvC1-Zqx.js.gz +0 -0
  312. package/dist/dashboard/assets/github-badge-BxpznFLt.js.gz +0 -0
  313. package/dist/dashboard/assets/index-CDzP3ovJ.js.gz +0 -0
  314. package/dist/dashboard/assets/inline-Ae13MQ5w.js.gz +0 -0
  315. package/dist/dashboard/assets/input-D23fo7cp.js.gz +0 -0
  316. package/dist/dashboard/assets/layout-CSsxjj5P.js.gz +0 -0
  317. package/dist/dashboard/assets/list-row-dmbwZMDY.js.gz +0 -0
  318. package/dist/dashboard/assets/live-BG1oUsZZ.js.gz +0 -0
  319. package/dist/dashboard/assets/mutation-DSF-e4ho.js.gz +0 -0
  320. package/dist/dashboard/assets/numbers-Bm_aECY7.js.gz +0 -0
  321. package/dist/dashboard/assets/query-keys-CF56nYIV.js.gz +0 -0
  322. package/dist/dashboard/assets/routing-CeA71n7G.js.gz +0 -0
  323. package/dist/dashboard/assets/select-KIT0rC6G.js.gz +0 -0
  324. package/dist/dashboard/assets/settings-B_2gkePf.js.gz +0 -0
  325. package/dist/dashboard/assets/stack-D7WOSzcX.js.gz +0 -0
  326. package/dist/dashboard/assets/state-BBxtKZOQ.js.gz +0 -0
  327. package/dist/dashboard/assets/switch-DNS_D5Ea.js.gz +0 -0
  328. package/dist/dashboard/assets/useMutation-BgoxdoMk.js.gz +0 -0
  329. /package/dist/dashboard/assets/{app-CaXZ9SCn.js → app-BaWd0puM.js} +0 -0
  330. /package/dist/dashboard/assets/{app-CaXZ9SCn.js.gz → app-BaWd0puM.js.gz} +0 -0
@@ -1,5 +1,5 @@
1
1
  // Drizzle-backed console persistence for network pools.
2
- import { and, asc, eq, sql } from "drizzle-orm";
2
+ import { and, asc, eq, inArray, sql } from "drizzle-orm";
3
3
  import type { CartethyiaDatabase } from "../../../persistence/db-handle";
4
4
  import { networkPools, poolRoutingSettings } from "../../../persistence/schema";
5
5
  import {
@@ -34,6 +34,7 @@ import {
34
34
  recoverNetworkPool,
35
35
  } from "../../../network/pool-health-machine";
36
36
  import { drainPoolByteDelta, recordPoolBytes } from "../../../network/pool/byte-accounting";
37
+ import * as poolCooldownStore from "../../../network/pool-cooldown-store";
37
38
  // Cloudflare's trace endpoint answers with plain `key=value` lines including
38
39
  // `ip=`, the address the request egressed from. Dialing it THROUGH the pool
39
40
  // therefore reports the pool's public address — which is what an operator
@@ -255,7 +256,7 @@ export class DrizzleNetworkPoolStore implements NetworkPoolStore {
255
256
  reason: error instanceof Error ? error.message : "Failed to build pool agent",
256
257
  };
257
258
  }
258
- return await this.dialProviderProbe(agent, poolId, providerId, poolId);
259
+ return await this.dialProviderProbe(agent, poolId, providerId, poolId, rec.endpoint);
259
260
  }
260
261
 
261
262
  /**
@@ -295,13 +296,19 @@ export class DrizzleNetworkPoolStore implements NetworkPoolStore {
295
296
  *
296
297
  * `label` is what the result reports (a pool id, or a pasted endpoint that
297
298
  * has no row yet). `poolId` is the row to bank the verdict on — omitted for
298
- * an ad-hoc probe, because there is nothing to attach it to.
299
+ * an ad-hoc probe, because there is nothing to attach it to. `endpoint` is
300
+ * the address the verdict describes, recorded beside it so
301
+ * `storedProviderProbe` can verify it later — it must be the pool's *current
302
+ * endpoint*, never the label: a verdict banked against the pool id fails the
303
+ * endpoint comparison on read and is hidden as "never probed", which is
304
+ * exactly the disappearing-verdict report.
299
305
  */
300
306
  private async dialProviderProbe(
301
307
  agent: PoolAgent,
302
308
  label: string,
303
309
  providerId: string,
304
310
  poolId?: string,
311
+ endpoint?: string,
305
312
  ): Promise<PoolProviderProbeResult> {
306
313
  const spec = await buildProviderProbeRequest(providerId);
307
314
  if (!spec) {
@@ -343,7 +350,7 @@ export class DrizzleNetworkPoolStore implements NetworkPoolStore {
343
350
  latencyMs,
344
351
  ...(verdict.reason === undefined ? {} : { reason: verdict.reason }),
345
352
  };
346
- if (poolId !== undefined) await this.persistProviderProbe(poolId, label, result);
353
+ if (poolId !== undefined) await this.persistProviderProbe(poolId, endpoint ?? label, result);
347
354
  return result;
348
355
  }
349
356
 
@@ -403,12 +410,24 @@ export class DrizzleNetworkPoolStore implements NetworkPoolStore {
403
410
  }
404
411
 
405
412
  async create(record: NetworkPoolRecord): Promise<void> {
406
- const kind = record.kind === "https" ? "http" : record.kind;
407
- await this.db.insert(networkPools).values({
413
+ await this.createMany([record]).then(() => undefined);
414
+ }
415
+
416
+ /**
417
+ * Inserts every record in one statement.
418
+ *
419
+ * A batch import used to be one round trip per proxy, so a long paste took
420
+ * minutes and the dialog sat there while the operator waited. Drizzle emits a
421
+ * single multi-row INSERT, which makes the cost independent of the paste
422
+ * length.
423
+ */
424
+ async createMany(records: readonly NetworkPoolRecord[]): Promise<number> {
425
+ if (records.length === 0) return 0;
426
+ const values = records.map((record) => ({
408
427
  id: record.id,
409
428
  tenantId: record.tenantId,
410
429
  createdAt: new Date(record.createdAt),
411
- kind,
430
+ kind: record.kind === "https" ? "http" : record.kind,
412
431
  endpointConfig: {
413
432
  endpoint: record.endpoint,
414
433
  ...(record.label ? { label: record.label } : {}),
@@ -423,7 +442,50 @@ export class DrizzleNetworkPoolStore implements NetworkPoolStore {
423
442
  consecutiveFailures: 0,
424
443
  ...(record.quotaBytes !== undefined ? { quotaBytes: record.quotaBytes } : {}),
425
444
  ...(record.family !== undefined ? { family: record.family } : {}),
426
- });
445
+ }));
446
+ await this.db.insert(networkPools).values(values);
447
+ return values.length;
448
+ }
449
+
450
+ /**
451
+ * Sets one status on every named pool in a single statement.
452
+ *
453
+ * Bulk enable/disable was a loop of per-row updates — one round trip each,
454
+ * and one snapshot invalidation each. A tenant-wide selection is exactly the
455
+ * case where that is felt.
456
+ */
457
+ async updateManyStatus(
458
+ tenantId: string,
459
+ poolIds: readonly string[],
460
+ status: NetworkPoolRecord["status"],
461
+ ): Promise<number> {
462
+ if (poolIds.length === 0) return 0;
463
+ const rows = await this.db
464
+ .update(networkPools)
465
+ .set({ status })
466
+ .where(
467
+ and(
468
+ eq(networkPools.tenantId, tenantId),
469
+ inArray(networkPools.id, [...poolIds]),
470
+ ),
471
+ )
472
+ .returning({ id: networkPools.id });
473
+ return rows.length;
474
+ }
475
+
476
+ /** Deletes every named pool in one statement. */
477
+ async deleteMany(tenantId: string, poolIds: readonly string[]): Promise<number> {
478
+ if (poolIds.length === 0) return 0;
479
+ const rows = await this.db
480
+ .delete(networkPools)
481
+ .where(
482
+ and(
483
+ eq(networkPools.tenantId, tenantId),
484
+ inArray(networkPools.id, [...poolIds]),
485
+ ),
486
+ )
487
+ .returning({ id: networkPools.id });
488
+ return rows.length;
427
489
  }
428
490
 
429
491
  async update(
@@ -513,6 +575,34 @@ export class DrizzleNetworkPoolStore implements NetworkPoolStore {
513
575
  return listNetworkPoolHealthEvents(this.db, tenantId, poolId);
514
576
  }
515
577
 
578
+ async dropProviderCooldownMarker(
579
+ tenantId: string,
580
+ poolId: string,
581
+ providerId: string,
582
+ onlyBlocked: boolean,
583
+ ): Promise<void> {
584
+ if ((await this.get(tenantId, poolId)) === undefined) return;
585
+ await poolCooldownStore.dropProviderCooldownMarker(this.db, poolId, providerId, onlyBlocked);
586
+ }
587
+
588
+ async dropAllProviderCooldownMarkers(
589
+ tenantId: string,
590
+ poolId: string,
591
+ ): Promise<void> {
592
+ if ((await this.get(tenantId, poolId)) === undefined) return;
593
+ await poolCooldownStore.dropAllProviderCooldownMarkers(this.db, poolId);
594
+ }
595
+
596
+ async recordProviderProbeCooldown(
597
+ tenantId: string,
598
+ poolId: string,
599
+ providerId: string,
600
+ marker: { until: number; reason: string; blocked?: boolean },
601
+ ): Promise<void> {
602
+ if ((await this.get(tenantId, poolId)) === undefined) return;
603
+ await poolCooldownStore.recordProbeVerdict(this.db, poolId, providerId, marker);
604
+ }
605
+
516
606
  async recover(tenantId: string, poolId: string): Promise<boolean> {
517
607
  return recoverNetworkPool(this.db, tenantId, poolId);
518
608
  }
package/src/main.ts CHANGED
@@ -1,167 +1,183 @@
1
- // Must run before any module that registers a route: it wires Elysia's TypeBox
2
- // bridge statically, which the source-run path needs under NODE_ENV=production.
3
- import "./boot/elysia-typebox-bridge";
4
- import { createGatewayApp, createGatewayShell } from "./boot/app";
5
- import { bootstrap } from "./boot/lifecycle";
6
- import type { CartethyiaBoot } from "./boot/lifecycle";
7
- import {
8
- resolveDrainToken,
9
- resolveIdleTimeout,
10
- resolveMaxBodyBytes,
11
- resolvePort,
12
- resolveProcessTopology,
13
- } from "./config/runtime";
14
- import { Manifest } from "elysia";
15
- import { log } from "./observability/logger";
16
-
17
- const port = resolvePort();
18
- const entryMemory = process.memoryUsage();
19
- log.info(
20
- `[startup] phase=entrypoint-ready elapsed_ms=${Math.round(process.uptime() * 1_000)} ` +
21
- `rss_mb=${(entryMemory.rss / 1024 / 1024).toFixed(1)} ` +
22
- `heap_used_mb=${(entryMemory.heapUsed / 1024 / 1024).toFixed(1)}`,
23
- );
24
-
25
- declare global {
26
- // eslint-disable-next-line no-var -- globalThis augmentation requires `var`
27
- var __cartethyiaBoot: CartethyiaBoot | undefined;
28
- // eslint-disable-next-line no-var
29
- var __cartethyiaSignalsRegistered: boolean | undefined;
30
- }
31
-
32
- const boot = Manifest.isCapturing()
33
- ? undefined
34
- : (globalThis.__cartethyiaBoot ??= await bootstrap());
35
-
36
- const app = boot
37
- ? createGatewayApp({
38
- mode: "production",
39
- db: boot.deps.db,
40
- proxyPreparer: boot.deps.proxyPreparer,
41
- resolveProviderAdapter: boot.deps.resolveProviderAdapter,
42
- byokUpstreamHosts: boot.deps.byokUpstreamHosts,
43
- networkBindingFactory: boot.deps.networkBindingFactory,
44
- ipAbuseProtection: boot.deps.ipAbuseProtection,
45
- trustedProxyBoundary: boot.deps.trustedProxyBoundary,
46
- poolSelector: boot.deps.poolSelector,
47
- snapshotService: boot.deps.snapshotService,
48
- readiness: boot.deps.readiness,
49
- telemetryBuffer: boot.deps.telemetryBuffer,
50
- resolveOAuthRefresher: boot.deps.resolveOAuthRefresher,
51
- oauthRefreshService: boot.deps.oauthRefreshService,
52
- maxBodyBytes: resolveMaxBodyBytes(),
53
- scheduledTasks: boot.deps.scheduledTasks,
54
- // The sampler is constructed in the dependency graph and already
55
- // sampling on its own interval; without passing it here the app cannot
56
- // bind its in-flight source, so `cartethyia_process_in_flight` stays
57
- // pinned at the constructor's `() => 0` placeholder forever while the
58
- // registry it was meant to read reports real flights.
59
- runtimeMetricsSampler: boot.deps.runtimeMetricsSampler,
60
- shutdownCoordinator: boot.shutdownCoordinator,
61
- // Signal-free graceful stop for platforms where a catchable signal cannot
62
- // be delivered (Windows). Off unless `CARTETHYIA_DRAIN_TOKEN` is set.
63
- ...(resolveDrainToken() !== undefined
64
- ? { drainToken: resolveDrainToken() as string, triggerDrain: () => shutdown("SIGTERM") }
65
- : {}),
66
- // The console mounts in every boot: without REDIS_URL it runs on the
67
- // in-process memory backend like the rest of the gateway.
68
- consoleApi: {
69
- db: boot.deps.db,
70
- accessResolver: () => undefined,
71
- routeSnapshotService: boot.deps.snapshotService,
72
- poolSelector: boot.deps.poolSelector,
73
- telemetryBuffer: boot.deps.telemetryBuffer,
74
- providerRegistry: boot.deps.providerRegistry,
75
- bundledModelCatalog: boot.deps.bundledModelCatalog,
76
- networkBindingFactory: boot.deps.networkBindingFactory,
77
- redis: boot.deps.redis,
78
- oauthRefreshService: boot.deps.oauthRefreshService,
79
- admissionService: boot.deps.admissionService,
80
- readRoutingAccountInflight: boot.deps.readRoutingAccountInflight,
81
- // The same three writers the shutdown coordinator drains, so a manual
82
- // flush from the console does exactly what a graceful stop would do
83
- // for the queues, minus the stop.
84
- flushBuffers: {
85
- flush: async () => {
86
- await boot.deps.telemetryBuffer.flush();
87
- await boot.deps.usageFlush.flush();
88
- await boot.deps.healthEventFlush.flush();
89
- },
90
- },
91
- },
92
- })
93
- : createGatewayShell();
94
- export { app };
95
-
96
- function shutdown(signal: "SIGINT" | "SIGTERM", reason: "SIGINT" | "SIGTERM" | "update" = signal): void {
97
- if (!boot) return;
98
- log.info(`[shutdown] ${signal} received, draining...`);
99
- // Derived from the coordinator's own budget, not a hand-kept literal: a
100
- // force-exit shorter than the drain would hard-kill a process that was about
101
- // to finish gracefully, truncating the in-flight responses the drain exists
102
- // to protect.
103
- const forceExit = setTimeout(() => {
104
- log.error("[shutdown] forced exit after drain budget elapsed");
105
- process.exit(1);
106
- }, boot.shutdownCoordinator.totalShutdownBudgetMs());
107
- forceExit.unref();
108
- boot.shutdownCoordinator
109
- .begin(reason)
110
- .then(() => {
111
- log.info("[shutdown] complete");
112
- process.exit(0);
113
- })
114
- .catch((error: unknown) => {
115
- log.error("[shutdown] failed", error as Error);
116
- process.exit(1);
117
- });
118
- }
119
-
120
- if (boot) {
121
- boot.server = app.listen(
122
- {
123
- port,
124
- // Cap idle sockets so a Slowloris/keep-alive-abuse client cannot
125
- // pin one of the server's `maxRequestBodySize`-budgeted sockets
126
- // indefinitely. 60 s aligns with the DB pool idle timeout so all
127
- // long-idle resources reap together.
128
- idleTimeout: resolveIdleTimeout(),
129
- // SO_REUSEPORT load-balancing, so several processes can share the port
130
- // without a reverse proxy. Tied to the declared topology rather than
131
- // left always-on: with one process declared, enabling it lets an
132
- // unrelated process on the same port silently take half the traffic —
133
- // which is worse than a bind error, because nothing reports it.
134
- reusePort: resolveProcessTopology().multiProcess,
135
- // 8 MiB cap on request bodies keeps a rogue client from allocating
136
- // gigabytes of memory in the ingress path before validation rejects.
137
- maxRequestBodySize: resolveMaxBodyBytes(),
138
- },
139
- () => {
140
- log.info(`Cartethyia listening on :${port} (Bun ${Bun.version})`);
141
- const memory = process.memoryUsage();
142
- log.info(
143
- `[startup] phase=listener-ready rss_mb=${(memory.rss / 1024 / 1024).toFixed(1)} ` +
144
- `heap_used_mb=${(memory.heapUsed / 1024 / 1024).toFixed(1)}`,
145
- );
146
- },
147
- );
148
- // Started here rather than inside the dependency builder: the first tick of
149
- // the lease sweep and the health sweep should not run before the listener
150
- // exists to serve traffic.
151
- boot.deps.scheduledTasks.start();
152
- if (!globalThis.__cartethyiaSignalsRegistered) {
153
- globalThis.__cartethyiaSignalsRegistered = true;
154
- process.on("SIGINT", () => shutdown("SIGINT"));
155
- process.on("SIGTERM", () => shutdown("SIGTERM"));
156
- // An in-place update signals SIGUSR2 before swapping the image so the old
157
- // process drains with the `update` reason and its callers are told the
158
- // replacement is seconds away instead of seeing a generic shutdown. Not
159
- // available on Windows, where the listener is simply never registered —
160
- // production runs in Linux containers, which is where an update happens.
161
- try {
162
- process.on("SIGUSR2", () => shutdown("SIGTERM", "update"));
163
- } catch {
164
- // Platform without SIGUSR2: update drains fall back to the SIGTERM notice.
165
- }
166
- }
167
- }
1
+ // Must run before any module that registers a route: it wires Elysia's TypeBox
2
+ // bridge statically, which the source-run path needs under NODE_ENV=production.
3
+ import "./boot/elysia-typebox-bridge";
4
+ import { createGatewayApp, createGatewayShell } from "./boot/app";
5
+ import { bootstrap } from "./boot/lifecycle";
6
+ import type { CartethyiaBoot } from "./boot/lifecycle";
7
+ import {
8
+ resolveDrainToken,
9
+ resolveIdleTimeout,
10
+ resolveMaxBodyBytes,
11
+ resolvePort,
12
+ resolveProcessTopology,
13
+ } from "./config/runtime";
14
+ import { Manifest } from "elysia";
15
+ import { log } from "./observability/logger";
16
+
17
+ const port = resolvePort();
18
+ const entryMemory = process.memoryUsage();
19
+ log.info(
20
+ `[startup] phase=entrypoint-ready elapsed_ms=${Math.round(process.uptime() * 1_000)} ` +
21
+ `rss_mb=${(entryMemory.rss / 1024 / 1024).toFixed(1)} ` +
22
+ `heap_used_mb=${(entryMemory.heapUsed / 1024 / 1024).toFixed(1)}`,
23
+ );
24
+
25
+ declare global {
26
+ // eslint-disable-next-line no-var -- globalThis augmentation requires `var`
27
+ var __cartethyiaBoot: CartethyiaBoot | undefined;
28
+ // eslint-disable-next-line no-var
29
+ var __cartethyiaSignalsRegistered: boolean | undefined;
30
+ }
31
+
32
+ const boot = Manifest.isCapturing()
33
+ ? undefined
34
+ : (globalThis.__cartethyiaBoot ??= await bootstrap());
35
+
36
+ const app = boot
37
+ ? createGatewayApp({
38
+ mode: "production",
39
+ db: boot.deps.db,
40
+ proxyPreparer: boot.deps.proxyPreparer,
41
+ resolveProviderAdapter: boot.deps.resolveProviderAdapter,
42
+ byokUpstreamHosts: boot.deps.byokUpstreamHosts,
43
+ networkBindingFactory: boot.deps.networkBindingFactory,
44
+ ipAbuseProtection: boot.deps.ipAbuseProtection,
45
+ trustedProxyBoundary: boot.deps.trustedProxyBoundary,
46
+ poolSelector: boot.deps.poolSelector,
47
+ snapshotService: boot.deps.snapshotService,
48
+ readiness: boot.deps.readiness,
49
+ telemetryBuffer: boot.deps.telemetryBuffer,
50
+ resolveOAuthRefresher: boot.deps.resolveOAuthRefresher,
51
+ oauthRefreshService: boot.deps.oauthRefreshService,
52
+ maxBodyBytes: resolveMaxBodyBytes(),
53
+ scheduledTasks: boot.deps.scheduledTasks,
54
+ // The sampler is constructed in the dependency graph and already
55
+ // sampling on its own interval; without passing it here the app cannot
56
+ // bind its in-flight source, so `cartethyia_process_in_flight` stays
57
+ // pinned at the constructor's `() => 0` placeholder forever while the
58
+ // registry it was meant to read reports real flights.
59
+ runtimeMetricsSampler: boot.deps.runtimeMetricsSampler,
60
+ shutdownCoordinator: boot.shutdownCoordinator,
61
+ // Signal-free graceful stop for platforms where a catchable signal cannot
62
+ // be delivered (Windows). Off unless `CARTETHYIA_DRAIN_TOKEN` is set.
63
+ ...(resolveDrainToken() !== undefined
64
+ ? { drainToken: resolveDrainToken() as string, triggerDrain: () => shutdown("SIGTERM") }
65
+ : {}),
66
+ // The console mounts in every boot: without REDIS_URL it runs on the
67
+ // in-process memory backend like the rest of the gateway.
68
+ consoleApi: {
69
+ db: boot.deps.db,
70
+ accessResolver: () => undefined,
71
+ routeSnapshotService: boot.deps.snapshotService,
72
+ poolSelector: boot.deps.poolSelector,
73
+ telemetryBuffer: boot.deps.telemetryBuffer,
74
+ providerRegistry: boot.deps.providerRegistry,
75
+ bundledModelCatalog: boot.deps.bundledModelCatalog,
76
+ networkBindingFactory: boot.deps.networkBindingFactory,
77
+ redis: boot.deps.redis,
78
+ oauthRefreshService: boot.deps.oauthRefreshService,
79
+ admissionService: boot.deps.admissionService,
80
+ readRoutingAccountInflight: boot.deps.readRoutingAccountInflight,
81
+ // The same three writers the shutdown coordinator drains, so a manual
82
+ // flush from the console does exactly what a graceful stop would do
83
+ // for the queues, minus the stop.
84
+ flushBuffers: {
85
+ flush: async () => {
86
+ await boot.deps.telemetryBuffer.flush();
87
+ await boot.deps.usageFlush.flush();
88
+ await boot.deps.healthEventFlush.flush();
89
+ },
90
+ },
91
+ },
92
+ })
93
+ : createGatewayShell();
94
+ export { app };
95
+
96
+ function shutdown(signal: "SIGINT" | "SIGTERM", reason: "SIGINT" | "SIGTERM" | "update" = signal): void {
97
+ if (!boot) return;
98
+ log.info(`[shutdown] ${signal} received, draining...`);
99
+ // Derived from the coordinator's own budget, not a hand-kept literal: a
100
+ // force-exit shorter than the drain would hard-kill a process that was about
101
+ // to finish gracefully, truncating the in-flight responses the drain exists
102
+ // to protect.
103
+ const forceExit = setTimeout(() => {
104
+ log.error("[shutdown] forced exit after drain budget elapsed");
105
+ process.exit(1);
106
+ }, boot.shutdownCoordinator.totalShutdownBudgetMs());
107
+ forceExit.unref();
108
+ boot.shutdownCoordinator
109
+ .begin(reason)
110
+ .then(() => {
111
+ log.info("[shutdown] complete");
112
+ process.exit(0);
113
+ })
114
+ .catch((error: unknown) => {
115
+ log.error("[shutdown] failed", error as Error);
116
+ process.exit(1);
117
+ });
118
+ }
119
+
120
+ if (boot) {
121
+ // `bun --hot` re-runs this module against the cached boot object; stop the
122
+ // previous listener before rebinding so a reload does not stack two servers
123
+ // on the same port.
124
+ if (boot.server && typeof boot.server.stop === "function") {
125
+ try {
126
+ boot.server.stop();
127
+ } catch {
128
+ // Best-effort on reload; the bind below would fail loudly if it stuck.
129
+ }
130
+ }
131
+ boot.server = app.listen(
132
+ {
133
+ port,
134
+ // Cap idle sockets so a Slowloris/keep-alive-abuse client cannot
135
+ // pin one of the server's `maxRequestBodySize`-budgeted sockets
136
+ // indefinitely. 60 s aligns with the DB pool idle timeout so all
137
+ // long-idle resources reap together.
138
+ idleTimeout: resolveIdleTimeout(),
139
+ // SO_REUSEPORT load-balancing, so several processes can share the port
140
+ // without a reverse proxy. Tied to the declared topology rather than
141
+ // left always-on: with one process declared, enabling it lets an
142
+ // unrelated process on the same port silently take half the traffic —
143
+ // which is worse than a bind error, because nothing reports it.
144
+ reusePort: resolveProcessTopology().multiProcess,
145
+ // 8 MiB cap on request bodies keeps a rogue client from allocating
146
+ // gigabytes of memory in the ingress path before validation rejects.
147
+ maxRequestBodySize: resolveMaxBodyBytes(),
148
+ },
149
+ () => {
150
+ log.info(`Cartethyia listening on :${port} (Bun ${Bun.version})`);
151
+ const memory = process.memoryUsage();
152
+ log.info(
153
+ `[startup] phase=listener-ready rss_mb=${(memory.rss / 1024 / 1024).toFixed(1)} ` +
154
+ `heap_used_mb=${(memory.heapUsed / 1024 / 1024).toFixed(1)}`,
155
+ );
156
+ },
157
+ );
158
+ // Started here rather than inside the dependency builder: the first tick of
159
+ // the lease sweep and the health sweep should not run before the listener
160
+ // exists to serve traffic.
161
+ boot.deps.scheduledTasks.start();
162
+ // Warm the route snapshot before the first request lands: the first /v1/*
163
+ // request otherwise pays the full catalog build synchronously (ten+ SELECTs).
164
+ // A failure here is a soft miss — the next request rebuilds as before.
165
+ void boot.deps.snapshotService.getSnapshot().catch((error: unknown) => {
166
+ log.warn("[startup] initial route snapshot warm failed", error as Error);
167
+ });
168
+ if (!globalThis.__cartethyiaSignalsRegistered) {
169
+ globalThis.__cartethyiaSignalsRegistered = true;
170
+ process.on("SIGINT", () => shutdown("SIGINT"));
171
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
172
+ // An in-place update signals SIGUSR2 before swapping the image so the old
173
+ // process drains with the `update` reason and its callers are told the
174
+ // replacement is seconds away instead of seeing a generic shutdown. Not
175
+ // available on Windows, where the listener is simply never registered —
176
+ // production runs in Linux containers, which is where an update happens.
177
+ try {
178
+ process.on("SIGUSR2", () => shutdown("SIGTERM", "update"));
179
+ } catch {
180
+ // Platform without SIGUSR2: update drains fall back to the SIGTERM notice.
181
+ }
182
+ }
183
+ }