cartethyia 2.7.4 → 2.7.5

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 (553) hide show
  1. package/.env.example +2 -0
  2. package/README.md +17 -5
  3. package/dist/dashboard/assets/{BannedPage-ChG5svFb.js → BannedPage-Cj15GsMp.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-Cj15GsMp.js.gz +0 -0
  5. package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js +1 -0
  6. package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolDetailPage-BIcWQun7.js → CliToolDetailPage-DZKmVuJN.js} +1 -1
  8. package/dist/dashboard/assets/CliToolDetailPage-DZKmVuJN.js.gz +0 -0
  9. package/dist/dashboard/assets/{CliToolsPage-Bf2VelDe.js → CliToolsPage-BIccZmeL.js} +1 -1
  10. package/dist/dashboard/assets/CliToolsPage-BIccZmeL.js.gz +0 -0
  11. package/dist/dashboard/assets/CombosPage-DEEKkYD-.js +1 -0
  12. package/dist/dashboard/assets/CombosPage-DEEKkYD-.js.gz +0 -0
  13. package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js +1 -0
  14. package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js.gz +0 -0
  15. package/dist/dashboard/assets/{CustomizationPage-ggeMSjte.js → CustomizationPage-BKj_r0rp.js} +1 -1
  16. package/dist/dashboard/assets/CustomizationPage-BKj_r0rp.js.gz +0 -0
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js +656 -0
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js.gz +0 -0
  19. package/dist/dashboard/assets/{LoginPage-U6AKaJcF.js → LoginPage-DkXb6dx-.js} +1 -1
  20. package/dist/dashboard/assets/LoginPage-DkXb6dx-.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-BMS2t8BA.js → ModelPicker-DnQPSIA8.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-DnQPSIA8.js.gz +0 -0
  23. package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js +1 -0
  24. package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js +5 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js.gz +0 -0
  27. package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js +1 -0
  28. package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js.gz +0 -0
  29. package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js +1 -0
  30. package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js.gz +0 -0
  31. package/dist/dashboard/assets/{ProxyPage-lsrx7NYg.js → ProxyPage-5SnUSy3j.js} +1 -1
  32. package/dist/dashboard/assets/ProxyPage-5SnUSy3j.js.gz +0 -0
  33. package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js +1 -0
  34. package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js.gz +0 -0
  35. package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js +1 -0
  36. package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js.gz +0 -0
  37. package/dist/dashboard/assets/{SetupPage-f0DE1h0-.js → SetupPage-WTa0sktC.js} +1 -1
  38. package/dist/dashboard/assets/SetupPage-WTa0sktC.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-Cq5tW7hW.js → StudioPage-BfAb5-o_.js} +3 -3
  40. package/dist/dashboard/assets/StudioPage-BfAb5-o_.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js.gz +0 -0
  43. package/dist/dashboard/assets/api-keys-l29JROPv.js +1 -0
  44. package/dist/dashboard/assets/api-keys-l29JROPv.js.gz +0 -0
  45. package/dist/dashboard/assets/app--PJWq8_C.css +2 -0
  46. package/dist/dashboard/assets/app--PJWq8_C.css.gz +0 -0
  47. package/dist/dashboard/assets/assert-C0K-c4eo.js +1 -0
  48. package/dist/dashboard/assets/assert-C0K-c4eo.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-D9CS6dZk.js → badge-DfbhQU4k.js} +1 -1
  50. package/dist/dashboard/assets/badge-DfbhQU4k.js.gz +0 -0
  51. package/dist/dashboard/assets/{banner-DympBNq4.js → banner-BllT4LcF.js} +1 -1
  52. package/dist/dashboard/assets/banner-BllT4LcF.js.gz +0 -0
  53. package/dist/dashboard/assets/brand/login_register.jpg +0 -0
  54. package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
  55. package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
  56. package/dist/dashboard/assets/brand/og_image.jpg +0 -0
  57. package/dist/dashboard/assets/{card-ibRWU_uT.js → card-CtkX66Wd.js} +1 -1
  58. package/dist/dashboard/assets/card-CtkX66Wd.js.gz +0 -0
  59. package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js +1 -0
  60. package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js.gz +0 -0
  61. package/dist/dashboard/assets/cli-tools-CQXVMn4P.js +1 -0
  62. package/dist/dashboard/assets/cli-tools-CQXVMn4P.js.gz +0 -0
  63. package/dist/dashboard/assets/{clipboard-button-DJm-9pz7.js → clipboard-button-BQqBQd2n.js} +1 -1
  64. package/dist/dashboard/assets/clipboard-button-BQqBQd2n.js.gz +0 -0
  65. package/dist/dashboard/assets/{dialog-8fiE6tec.js → dialog-BEK7KP0H.js} +1 -1
  66. package/dist/dashboard/assets/dialog-BEK7KP0H.js.gz +0 -0
  67. package/dist/dashboard/assets/{drawer-CqGYeOrd.js → drawer-BrTEZ8zu.js} +1 -1
  68. package/dist/dashboard/assets/drawer-BrTEZ8zu.js.gz +0 -0
  69. package/dist/dashboard/assets/entry-CHWxbS56.js +1 -0
  70. package/dist/dashboard/assets/entry-CHWxbS56.js.gz +0 -0
  71. package/dist/dashboard/assets/{entry-BSAsYsmV.js → entry-EBHJ3DrE.js} +1 -1
  72. package/dist/dashboard/assets/entry-EBHJ3DrE.js.gz +0 -0
  73. package/dist/dashboard/assets/entry-LCS85hiq.js +4 -0
  74. package/dist/dashboard/assets/entry-LCS85hiq.js.gz +0 -0
  75. package/dist/dashboard/assets/{formatters-D-MTJxMr.js → formatters-BAACA727.js} +1 -1
  76. package/dist/dashboard/assets/formatters-BAACA727.js.gz +0 -0
  77. package/dist/dashboard/assets/{github-badge-CPK47xQx.js → github-badge-YSCE4UC2.js} +1 -1
  78. package/dist/dashboard/assets/github-badge-YSCE4UC2.js.gz +0 -0
  79. package/dist/dashboard/assets/{index-DBNPOfhV.js → index-FQlj0HUT.js} +2 -2
  80. package/dist/dashboard/assets/index-FQlj0HUT.js.gz +0 -0
  81. package/dist/dashboard/assets/{inline-BT0zntEr.js → inline-C4gU5iOL.js} +1 -1
  82. package/dist/dashboard/assets/inline-C4gU5iOL.js.gz +0 -0
  83. package/dist/dashboard/assets/{input-Da2ZViWg.js → input-BHHX_Tmm.js} +1 -1
  84. package/dist/dashboard/assets/input-BHHX_Tmm.js.gz +0 -0
  85. package/dist/dashboard/assets/{layout-BXNeLw6M.js → layout-CajrYUnD.js} +1 -1
  86. package/dist/dashboard/assets/layout-CajrYUnD.js.gz +0 -0
  87. package/dist/dashboard/assets/{list-row-BycOrrIV.js → list-row-Bx2I7XUe.js} +1 -1
  88. package/dist/dashboard/assets/list-row-Bx2I7XUe.js.gz +0 -0
  89. package/dist/dashboard/assets/live-DNNqRbj2.js +1 -0
  90. package/dist/dashboard/assets/live-DNNqRbj2.js.gz +0 -0
  91. package/dist/dashboard/assets/network-C5UBACrJ.js +1 -0
  92. package/dist/dashboard/assets/network-C5UBACrJ.js.gz +0 -0
  93. package/dist/dashboard/assets/numbers-CUWAZK4J.js +2 -0
  94. package/dist/dashboard/assets/numbers-CUWAZK4J.js.gz +0 -0
  95. package/dist/dashboard/assets/pagination-Xsi9Qx04.js +1 -0
  96. package/dist/dashboard/assets/pagination-Xsi9Qx04.js.gz +0 -0
  97. package/dist/dashboard/assets/quota-DvQbD_sr.js +1 -0
  98. package/dist/dashboard/assets/quota-DvQbD_sr.js.gz +0 -0
  99. package/dist/dashboard/assets/routing-in0f_IfK.js +1 -0
  100. package/dist/dashboard/assets/routing-in0f_IfK.js.gz +0 -0
  101. package/dist/dashboard/assets/{select-BIYIBkMU.js → select-BLULNqTp.js} +1 -1
  102. package/dist/dashboard/assets/select-BLULNqTp.js.gz +0 -0
  103. package/dist/dashboard/assets/settings-CcGczj6I.js +1 -0
  104. package/dist/dashboard/assets/settings-CcGczj6I.js.gz +0 -0
  105. package/dist/dashboard/assets/shield-BMQNxORh.js +1 -0
  106. package/dist/dashboard/assets/shield-BMQNxORh.js.gz +0 -0
  107. package/dist/dashboard/assets/{stack-DfgnFDWY.js → stack-D2lIR0tg.js} +1 -1
  108. package/dist/dashboard/assets/stack-D2lIR0tg.js.gz +0 -0
  109. package/dist/dashboard/assets/{state-CkfdYWel.js → state-DTARjfiW.js} +1 -1
  110. package/dist/dashboard/assets/state-DTARjfiW.js.gz +0 -0
  111. package/dist/dashboard/assets/{switch-SYm237HG.js → switch-c0oPSnqW.js} +1 -1
  112. package/dist/dashboard/assets/switch-c0oPSnqW.js.gz +0 -0
  113. package/dist/dashboard/assets/{useMutation-BQqCRNIi.js → useMutation-m8WGNPrz.js} +1 -1
  114. package/dist/dashboard/assets/useMutation-m8WGNPrz.js.gz +0 -0
  115. package/dist/dashboard/console-backgrounds.jpg +0 -0
  116. package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
  117. package/dist/dashboard/index.html +2 -2
  118. package/dist/dashboard/index.html.gz +0 -0
  119. package/dist/dashboard/precompressed-assets.json +56 -55
  120. package/migrations/full-postgres/0036_system_prompt_presets_refs.sql +10 -0
  121. package/migrations/full-postgres/0037_share_concurrency_scope.sql +3 -0
  122. package/migrations/full-postgres/0038_add_accounts_quota_snapshot.sql +7 -0
  123. package/migrations/full-postgres/0039_add_telemetry_provider_model_indexes.sql +13 -0
  124. package/migrations/full-postgres/0040_add_models_dialect.sql +3 -0
  125. package/migrations/full-postgres/0041_add_daily_checkin_ledger_account_fk.sql +7 -0
  126. package/migrations/lite-sqlite3/0036_system_prompt_presets_refs.sql +14 -0
  127. package/migrations/lite-sqlite3/0037_share_concurrency_scope.sql +3 -0
  128. package/migrations/lite-sqlite3/0038_add_accounts_quota_snapshot.sql +4 -0
  129. package/migrations/lite-sqlite3/0039_add_telemetry_provider_model_indexes.sql +6 -0
  130. package/migrations/lite-sqlite3/0040_add_models_dialect.sql +3 -0
  131. package/migrations/lite-sqlite3/0041_add_daily_checkin_ledger_account_fk.sql +26 -0
  132. package/package.json +4 -6
  133. package/scripts/cli/update.ts +15 -41
  134. package/src/boot/app.ts +58 -2
  135. package/src/boot/dependencies.ts +786 -732
  136. package/src/boot/lifecycle.ts +11 -12
  137. package/src/config/cooldowns.ts +34 -55
  138. package/src/config/network.ts +1 -1
  139. package/src/config/package-paths.ts +1 -1
  140. package/src/config/providers.ts +13 -14
  141. package/src/config/resources.ts +0 -5
  142. package/src/config/runtime.ts +2 -5
  143. package/src/config/spec.ts +26 -6
  144. package/src/console/api-keys/ban-constants.ts +38 -0
  145. package/src/console/api-keys/contracts.ts +71 -62
  146. package/src/console/api-keys/routes.ts +48 -12
  147. package/src/console/auth/access.ts +4 -4
  148. package/src/console/auth/account-reset.ts +1 -1
  149. package/src/console/auth/audit-recorder.ts +1 -1
  150. package/src/console/auth/lockout.ts +7 -3
  151. package/src/console/auth/row-coercion.ts +57 -12
  152. package/src/console/auth/session-resolver.ts +1 -1
  153. package/src/console/auth/session-service.ts +8 -2
  154. package/src/console/auth/session.ts +37 -31
  155. package/src/console/auth/setup-service.ts +4 -3
  156. package/src/console/auth/user-store.ts +1 -1
  157. package/src/console/backup/9router-map.ts +0 -3
  158. package/src/console/backup/contracts.ts +9 -9
  159. package/src/console/backup/export-endpoint.ts +5 -6
  160. package/src/console/backup/nine-router.ts +1 -7
  161. package/src/console/backup/routes.ts +29 -33
  162. package/src/console/backup/service.ts +468 -468
  163. package/src/console/backup/store.ts +47 -29
  164. package/src/console/backup/validate.ts +11 -1
  165. package/src/console/cli-tools/contracts.ts +7 -7
  166. package/src/console/cli-tools/injectors/cline.ts +110 -110
  167. package/src/console/cli-tools/injectors/codex.ts +181 -181
  168. package/src/console/cli-tools/injectors/copilot.ts +108 -108
  169. package/src/console/cli-tools/injectors/driver.ts +247 -247
  170. package/src/console/cli-tools/injectors/jcode.ts +115 -115
  171. package/src/console/cli-tools/injectors/openclaw.ts +161 -161
  172. package/src/console/cli-tools/injectors/opencode.ts +153 -153
  173. package/src/console/cli-tools/routes.ts +1 -1
  174. package/src/console/cli-tools/service.ts +1 -1
  175. package/src/console/cli-tools/store.ts +2 -2
  176. package/src/console/console-router.ts +32 -0
  177. package/src/console/dashboard-assets.ts +9 -9
  178. package/src/console/domain-registration.ts +16 -2
  179. package/src/console/inferhub/client.ts +0 -7
  180. package/src/console/inferhub/key-store.ts +1 -1
  181. package/src/console/inferhub/normalize.ts +2 -2
  182. package/src/console/inferhub/routes.ts +1 -1
  183. package/src/console/inferhub/types.ts +1 -1
  184. package/src/console/observability/contracts.ts +1 -1
  185. package/src/console/observability/live.ts +31 -18
  186. package/src/console/observability/logs.ts +1 -1
  187. package/src/console/observability/sse.ts +62 -8
  188. package/src/console/observability/store.ts +20 -4
  189. package/src/console/providers/catalog/catalog-projections.ts +1 -1
  190. package/src/console/providers/catalog/contracts.ts +39 -0
  191. package/src/console/providers/catalog/model-operations.ts +1 -1
  192. package/src/console/providers/catalog/provider-operations.ts +95 -17
  193. package/src/console/providers/catalog/public-model-store.ts +0 -0
  194. package/src/console/providers/catalog/routes.ts +27 -3
  195. package/src/console/providers/catalog/store.ts +147 -8
  196. package/src/console/providers/detail/routes.ts +1 -1
  197. package/src/console/providers/oauth/callback-listener.ts +100 -18
  198. package/src/console/providers/oauth/routes.ts +92 -13
  199. package/src/console/quota/account-quota-global.ts +41 -3
  200. package/src/console/quota/account-quota-shared.ts +10 -3
  201. package/src/console/quota/account-quota-tenant.ts +259 -66
  202. package/src/console/quota/daily-growth-job.ts +308 -0
  203. package/src/console/quota/native-identity.ts +2 -2
  204. package/src/console/quota/quota-view.ts +1 -1
  205. package/src/console/remote-support/routes.ts +1 -1
  206. package/src/console/remote-support/service.ts +2 -2
  207. package/src/console/routing/model/contracts.ts +1 -1
  208. package/src/console/routing/pools/contracts.ts +2 -2
  209. package/src/console/routing/pools/import-parse.ts +3 -3
  210. package/src/console/routing/pools/relay-deploy.ts +3 -3
  211. package/src/console/settings/contracts.ts +29 -2
  212. package/src/console/settings/store.ts +20 -0
  213. package/src/console/share/share-router.ts +100 -32
  214. package/src/console/share/share-stats.ts +4 -4
  215. package/src/console/share/share-usage.ts +1 -1
  216. package/src/console/shared/errors.ts +2 -2
  217. package/src/console/shared/object.ts +18 -0
  218. package/src/console/shared/read-through-cache.ts +4 -4
  219. package/src/console/shared/transaction.ts +14 -0
  220. package/src/console/system-prompt/contracts.ts +89 -12
  221. package/src/console/system-prompt/routes.ts +98 -45
  222. package/src/console/updates/apply-update.ts +244 -0
  223. package/src/console/updates/routes.ts +252 -97
  224. package/src/main.ts +11 -5
  225. package/src/network/http2-bidi.ts +1 -1
  226. package/src/network/ip-utils.ts +298 -0
  227. package/src/network/outbound-fetch.ts +1 -1
  228. package/src/network/pool/agent.ts +7 -6
  229. package/src/network/pool/byte-accounting.ts +1 -1
  230. package/src/network/pool/resolver.ts +16 -12
  231. package/src/network/pool/selector.ts +150 -136
  232. package/src/network/pool-health-machine.ts +1 -1
  233. package/src/network/rate-limit-verdict.ts +1 -1
  234. package/src/network/retry.ts +1 -1
  235. package/src/network/ssrf.ts +20 -71
  236. package/src/observability/event-log.ts +2 -2
  237. package/src/observability/event-query.ts +59 -291
  238. package/src/observability/log-sanitize.ts +1 -1
  239. package/src/observability/metrics.ts +4 -4
  240. package/src/observability/payload-capture.ts +6 -5
  241. package/src/observability/payload-store.ts +1 -1
  242. package/src/observability/runtime-metrics.ts +1 -1
  243. package/src/observability/telemetry-buffer.ts +12 -3
  244. package/src/observability/telemetry-reconciliation.ts +3 -3
  245. package/src/observability/token-speed.ts +1 -1
  246. package/src/persistence/connection-url.ts +1 -1
  247. package/src/persistence/db-handle.ts +1 -1
  248. package/src/persistence/db-pg.ts +2 -2
  249. package/src/persistence/db-sqlite.ts +3 -3
  250. package/src/persistence/dialect.ts +5 -1
  251. package/src/persistence/readiness.ts +1 -1
  252. package/src/persistence/redis.ts +1 -1
  253. package/src/persistence/schema.ts +113 -40
  254. package/src/persistence/shared-cache.ts +2 -2
  255. package/src/persistence/sort-order.ts +1 -1
  256. package/src/persistence/stores/api-key-store.ts +10 -1
  257. package/src/persistence/stores/share-store.ts +20 -8
  258. package/src/persistence/system-prompt-store.ts +63 -18
  259. package/src/persistence/tenant-preferences.ts +1 -1
  260. package/src/persistence/ttl-cache.ts +3 -3
  261. package/src/persistence/usage-flush.ts +7 -2
  262. package/src/protocol/canonical-model.ts +1 -1
  263. package/src/protocol/codecs.ts +6 -10
  264. package/src/protocol/dsml.ts +3 -49
  265. package/src/protocol/image-clamp.ts +2 -2
  266. package/src/protocol/reasoning-text.ts +1 -1
  267. package/src/protocol/reasoning.ts +7 -52
  268. package/src/protocol/registry.ts +50 -15
  269. package/src/protocol/request/chat.ts +46 -19
  270. package/src/protocol/request/codex.ts +1 -1
  271. package/src/protocol/request/messages.ts +46 -14
  272. package/src/protocol/request/responses.ts +19 -10
  273. package/src/protocol/response/chat.ts +25 -10
  274. package/src/protocol/response/codex.ts +1 -1
  275. package/src/protocol/response/messages.ts +1 -1
  276. package/src/protocol/response/omp-tool-call-markup.ts +1 -1
  277. package/src/protocol/response/responses.ts +1 -1
  278. package/src/protocol/sse-decoder.ts +50 -1
  279. package/src/protocol/stop-reason.ts +2 -2
  280. package/src/protocol/think-split.ts +347 -0
  281. package/src/protocol/translation/attachment-integrity.ts +3 -3
  282. package/src/protocol/translation/cache-controls.ts +8 -20
  283. package/src/protocol/translation/capabilities.ts +6 -6
  284. package/src/protocol/translation/dialect/deepseek.ts +10 -0
  285. package/src/protocol/translation/dialect/inband.ts +39 -0
  286. package/src/protocol/translation/dialect/model-thinking.ts +89 -0
  287. package/src/protocol/translation/dialect/qwen.ts +114 -0
  288. package/src/protocol/translation/dialect/resolver.ts +83 -0
  289. package/src/protocol/translation/dialect/rules.ts +93 -0
  290. package/src/protocol/translation/dialect/thinking-ladder.ts +245 -0
  291. package/src/protocol/translation/dialect/types.ts +58 -0
  292. package/src/protocol/translation/quirks.ts +18 -4
  293. package/src/protocol/translation/thinking.ts +12 -252
  294. package/src/protocol/translation/tool-id.ts +3 -3
  295. package/src/protocol/translation/tool-repair.ts +1 -1
  296. package/src/protocol/upstream/post-json.ts +6 -1
  297. package/src/providers/adapters/compatible-adapter.ts +42 -3
  298. package/src/providers/authentication/jwt-validator.ts +0 -5
  299. package/src/providers/authentication/oauth-client.ts +254 -253
  300. package/src/providers/authentication/oauth-flow-store.ts +42 -7
  301. package/src/providers/bundled-discovery-providers.ts +6 -30
  302. package/src/providers/configured-openai-providers.ts +82 -97
  303. package/src/providers/default-registry.ts +23 -35
  304. package/src/providers/discovery/catalog/catalog-format.ts +13 -2
  305. package/src/providers/discovery/discovery-types.ts +0 -1
  306. package/src/providers/discovery/openai-model-discovery.ts +2 -2
  307. package/src/providers/discovery/probe-phases.ts +48 -13
  308. package/src/providers/discovery/probing-service.ts +6 -8
  309. package/src/providers/integrations/agentrouter/agentrouter.ts +12 -2
  310. package/src/providers/integrations/antigravity/antigravity-oauth.ts +0 -1
  311. package/src/providers/integrations/antigravity/antigravity-protocol.ts +0 -14
  312. package/src/providers/integrations/antigravity/antigravity.ts +2 -2
  313. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
  314. package/src/providers/integrations/buddy/buddy-chat-shared.ts +20 -4
  315. package/src/providers/integrations/buddy/buddy-cli-headers.ts +19 -52
  316. package/src/providers/integrations/buddy/buddy-identity.ts +86 -0
  317. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +1 -1
  318. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +1 -2
  319. package/src/providers/integrations/buddy/codebuddy/provider.ts +4 -15
  320. package/src/providers/integrations/buddy/codebuddy/shared.ts +30 -9
  321. package/src/providers/integrations/buddy/workbuddy/provider.ts +5 -9
  322. package/src/providers/integrations/buddy/workbuddy/shared.ts +21 -3
  323. package/src/providers/integrations/claude/claude-cch.ts +148 -148
  324. package/src/providers/integrations/claude/fallback-credit.ts +3 -7
  325. package/src/providers/integrations/cline/cline.ts +1 -1
  326. package/src/providers/integrations/codex/codex-device-code.ts +101 -101
  327. package/src/providers/integrations/codex/codex-oauth.ts +0 -1
  328. package/src/providers/integrations/commandcode/commandcode.ts +75 -115
  329. package/src/providers/integrations/commandcode/quota.ts +131 -51
  330. package/src/providers/integrations/cursor/cursor-http2.ts +0 -3
  331. package/src/providers/integrations/cursor/cursor-quota.ts +2 -6
  332. package/src/providers/integrations/cursor/cursor-request.ts +0 -87
  333. package/src/providers/integrations/cursor/cursor-shared.ts +0 -4
  334. package/src/providers/integrations/cursor/cursor.ts +38 -5
  335. package/src/providers/integrations/cursor/protocol/framing.ts +0 -56
  336. package/src/providers/integrations/cursor/protocol/messages.ts +0 -96
  337. package/src/providers/integrations/factory/factory.ts +9 -9
  338. package/src/providers/integrations/github/github-discovery.ts +55 -10
  339. package/src/providers/integrations/github/github-oauth.ts +6 -5
  340. package/src/providers/integrations/github/github.ts +41 -14
  341. package/src/providers/integrations/grok/grok-oauth.ts +261 -261
  342. package/src/providers/integrations/grok/grok-quota-frame.ts +1 -1
  343. package/src/providers/integrations/grok/grok-quota.ts +16 -7
  344. package/src/providers/integrations/grok/grok-ratelimit-store.ts +0 -5
  345. package/src/providers/integrations/grok/grok-ratelimit.ts +3 -0
  346. package/src/providers/integrations/grok/grok.ts +86 -36
  347. package/src/providers/integrations/kilo/kilo-oauth.ts +1 -1
  348. package/src/providers/integrations/kiro/kiro-cache.ts +31 -0
  349. package/src/providers/integrations/kiro/kiro-catalog.ts +31 -5
  350. package/src/providers/integrations/kiro/kiro-discovery.ts +47 -43
  351. package/src/providers/integrations/kiro/kiro-machine-id.ts +6 -3
  352. package/src/providers/integrations/kiro/kiro-oauth.ts +523 -255
  353. package/src/providers/integrations/kiro/kiro-quota.ts +35 -47
  354. package/src/providers/integrations/kiro/kiro-request.ts +8 -11
  355. package/src/providers/integrations/kiro/kiro-stream.ts +13 -0
  356. package/src/providers/integrations/kiro/kiro.ts +81 -68
  357. package/src/providers/integrations/muse/muse-oauth.ts +0 -1
  358. package/src/providers/integrations/muse/muse-quota.ts +0 -4
  359. package/src/providers/integrations/opencode/fingerprint.ts +7 -5
  360. package/src/providers/integrations/opencode/provider.ts +17 -24
  361. package/src/providers/integrations/qoder/qoder-catalog.ts +85 -8
  362. package/src/providers/integrations/qoder/qoder-oauth.ts +165 -25
  363. package/src/providers/integrations/qoder/qoder-quota.ts +48 -6
  364. package/src/providers/integrations/qoder/qoder-realm.ts +10 -31
  365. package/src/providers/integrations/qoder/qoder-sse.ts +159 -0
  366. package/src/providers/integrations/qoder/qoder-umid.ts +21 -25
  367. package/src/providers/integrations/qoder/qoder-virtual.ts +12 -0
  368. package/src/providers/integrations/qoder/qoder.ts +354 -62
  369. package/src/providers/integrations/shared/connect.ts +0 -54
  370. package/src/providers/integrations/shared/hash.ts +15 -0
  371. package/src/providers/integrations/shared/session-cache.ts +136 -0
  372. package/src/providers/integrations/trae/trae-cache.ts +7 -87
  373. package/src/providers/integrations/trae/trae-catalog.ts +1 -6
  374. package/src/providers/integrations/trae/trae-protocol.ts +17 -8
  375. package/src/providers/integrations/trae/trae-realm.ts +0 -7
  376. package/src/providers/integrations/trae/trae.ts +53 -10
  377. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  378. package/src/providers/integrations/zcode-start-plan/gifts.ts +265 -0
  379. package/src/providers/integrations/zcode-start-plan/oauth.ts +154 -0
  380. package/src/providers/integrations/zcode-start-plan/provider.ts +131 -0
  381. package/src/providers/integrations/zcode-start-plan/quota.ts +152 -0
  382. package/src/providers/integrations/zcode-start-plan/zcode_prompt.json +10 -0
  383. package/src/providers/operations/account-error-classification.ts +131 -116
  384. package/src/providers/operations/account-health-service.ts +31 -39
  385. package/src/providers/operations/account-reset-service.ts +58 -1
  386. package/src/providers/operations/cli-platform.ts +31 -5
  387. package/src/providers/operations/client-version-resolver.ts +4 -6
  388. package/src/providers/operations/client-versions.ts +10 -7
  389. package/src/providers/operations/provider-catalog-seeder.ts +2 -0
  390. package/src/providers/operations/provider-catalog-service.ts +18 -2
  391. package/src/providers/operations/provider-credential-service.ts +19 -2
  392. package/src/providers/provider-contracts.ts +18 -7
  393. package/src/providers/provider-metadata.ts +17 -2
  394. package/src/providers/provider-model-definition.ts +22 -1
  395. package/src/providers/provider-probe.ts +4 -2
  396. package/src/providers/provider-usage.ts +81 -16
  397. package/src/providers/quota/cache.ts +29 -0
  398. package/src/providers/quota/quota-contracts.ts +4 -0
  399. package/src/providers/quota/refresh.ts +27 -4
  400. package/src/providers/quota/snapshot.ts +94 -0
  401. package/src/providers/search/search-provider.ts +1 -3
  402. package/src/providers/search/search-providers.ts +2 -1
  403. package/src/security/abuse.ts +4 -4
  404. package/src/security/admission/service.ts +2 -2
  405. package/src/security/admission/ttl.ts +61 -61
  406. package/src/security/api-key-auth.ts +16 -4
  407. package/src/security/client-router-fingerprint.ts +4 -4
  408. package/src/security/credential-codec.ts +3 -3
  409. package/src/security/csrf.ts +2 -2
  410. package/src/security/ip-boundary.ts +16 -189
  411. package/src/security/model-access-rule.ts +1 -1
  412. package/src/security/outbound-headers.ts +18 -0
  413. package/src/shared/invoke-guard.ts +47 -0
  414. package/src/transport/contracts.ts +13 -4
  415. package/src/transport/dispatch/attempt-finalize.ts +1 -1
  416. package/src/transport/dispatch/capture-policy.ts +34 -37
  417. package/src/transport/dispatch/empty-stream.ts +23 -20
  418. package/src/transport/dispatch/generation.ts +25 -45
  419. package/src/transport/dispatch/native-shared.ts +127 -0
  420. package/src/transport/dispatch/proxy-request.ts +3 -1
  421. package/src/transport/dispatch/responses-compact.ts +25 -46
  422. package/src/transport/dispatch/streaming/encoder.ts +18 -72
  423. package/src/transport/dispatch/streaming/finalize.ts +2 -2
  424. package/src/transport/dispatch/streaming/stream.ts +7 -3
  425. package/src/transport/dispatch/streaming/thinking-loop.ts +1 -1
  426. package/src/transport/dispatch/systemone.ts +20 -48
  427. package/src/transport/dispatch/token-count.ts +41 -38
  428. package/src/transport/dispatch/websearch.ts +19 -45
  429. package/src/transport/drain-endpoint.ts +1 -1
  430. package/src/transport/failure-policy.ts +6 -0
  431. package/src/transport/guards/auth-guard.ts +0 -6
  432. package/src/transport/guards/body-policy.ts +3 -3
  433. package/src/transport/guards/headroom-guard.ts +1 -1
  434. package/src/transport/guards/pipeline.ts +1 -1
  435. package/src/transport/guards/request-context.ts +12 -17
  436. package/src/transport/request/preparer.ts +286 -7
  437. package/src/transport/request/rtk/compress-request.ts +3 -3
  438. package/src/transport/request/rtk/rtk-constants.ts +2 -2
  439. package/src/transport/request/rtk/rtk-filters.ts +8 -8
  440. package/src/transport/request/state.ts +1 -13
  441. package/src/transport/request/system-prompt-injection.ts +74 -11
  442. package/src/transport/request/tenant-preferences.ts +22 -0
  443. package/src/transport/routing/candidate-index.ts +2 -2
  444. package/src/transport/routing/round-robin.ts +1 -1
  445. package/src/transport/routing/route-catalog.ts +17 -18
  446. package/src/transport/routing/route-model.ts +126 -33
  447. package/src/transport/routing/router.ts +152 -37
  448. package/src/transport/shutdown-notice.ts +2 -2
  449. package/src/transport/surface/adapters.ts +3 -3
  450. package/src/transport/surface/chat/encode.ts +7 -7
  451. package/src/transport/surface/chat/parse.ts +7 -7
  452. package/src/transport/surface/completion.ts +2 -2
  453. package/src/transport/surface/content-parts.ts +1 -1
  454. package/src/transport/surface/dialects.ts +2 -2
  455. package/src/transport/surface/messages/parse.ts +3 -3
  456. package/src/transport/surface/responses/adapter.ts +1 -1
  457. package/src/transport/surface/responses/errors.ts +0 -9
  458. package/src/transport/vision-handoff-cache.ts +58 -0
  459. package/src/transport/vision-handoff.ts +376 -0
  460. package/src/workers/account-identity-sweep.ts +2 -2
  461. package/src/workers/buddy-growth-tasks.ts +392 -346
  462. package/src/workers/daily-checkin.ts +102 -49
  463. package/src/workers/maintenance-lease.ts +2 -35
  464. package/src/workers/oauth-refresh-sweep.ts +8 -1
  465. package/src/workers/quota-refresh-sweep.ts +120 -82
  466. package/src/workers/scheduler.ts +67 -4
  467. package/src/workers/sweep-runner.ts +24 -5
  468. package/src/workers/zcode-gift-claim.ts +108 -0
  469. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js +0 -1
  470. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js.gz +0 -0
  471. package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
  472. package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
  473. package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
  474. package/dist/dashboard/assets/CombosPage-DCgm5g6g.js +0 -1
  475. package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
  476. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +0 -1
  477. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
  478. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
  479. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js +0 -1
  480. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
  481. package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
  482. package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
  483. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +0 -1
  484. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
  485. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +0 -5
  486. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
  487. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +0 -1
  488. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
  489. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +0 -1
  490. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
  491. package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
  492. package/dist/dashboard/assets/QuotaPage-L0skzML8.js +0 -1
  493. package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
  494. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +0 -1
  495. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
  496. package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
  497. package/dist/dashboard/assets/SortableList-ClzpwyET.js +0 -1
  498. package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
  499. package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
  500. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +0 -56
  501. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
  502. package/dist/dashboard/assets/api-keys-C38iXoO4.js +0 -1
  503. package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
  504. package/dist/dashboard/assets/app-a159k0ub.css +0 -2
  505. package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
  506. package/dist/dashboard/assets/assert-DbRA8EG2.js +0 -15
  507. package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
  508. package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
  509. package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
  510. package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
  511. package/dist/dashboard/assets/cli-tools-63qBGaRw.js +0 -1
  512. package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
  513. package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
  514. package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
  515. package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
  516. package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
  517. package/dist/dashboard/assets/entry-CiIQULZD.js +0 -4
  518. package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
  519. package/dist/dashboard/assets/entry-Daz0lJ-W.js +0 -1
  520. package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
  521. package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
  522. package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
  523. package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
  524. package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
  525. package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
  526. package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
  527. package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
  528. package/dist/dashboard/assets/live-DCQNZpF7.js +0 -1
  529. package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
  530. package/dist/dashboard/assets/network-9Yj2ZyLo.js +0 -1
  531. package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
  532. package/dist/dashboard/assets/numbers-CEagj6ZZ.js +0 -2
  533. package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
  534. package/dist/dashboard/assets/quota-Caw7wqci.js +0 -1
  535. package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
  536. package/dist/dashboard/assets/routing-CO9zCUus.js +0 -1
  537. package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
  538. package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
  539. package/dist/dashboard/assets/settings-jS3-PNSA.js +0 -1
  540. package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
  541. package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
  542. package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
  543. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js +0 -1
  544. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  545. package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
  546. package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
  547. package/src/network/response-headers.ts +0 -39
  548. package/src/protocol/response/dsml.ts +0 -38
  549. package/src/providers/integrations/xiaomi-mimo/think-stream.ts +0 -229
  550. /package/dist/dashboard/assets/{app-BayTTy01.js → app-Bgd3zANQ.js} +0 -0
  551. /package/dist/dashboard/assets/{app-BayTTy01.js.gz → app-Bgd3zANQ.js.gz} +0 -0
  552. /package/dist/dashboard/assets/brand/{og_bansos.webp → og_bansos_initial.webp} +0 -0
  553. /package/dist/dashboard/assets/brand/{og_image.webp → og_image_initial.webp} +0 -0
@@ -0,0 +1,244 @@
1
+ /**
2
+ * The detached update flow — the one implementation the console route and the
3
+ * CLI (`carte update`, console menu) share.
4
+ *
5
+ * npm cannot replace this package's directory while a process is executing out
6
+ * of it (EBUSY on Windows), and the deployment model has no process supervisor
7
+ * that could restart after an in-process install. So the installer MUST be a
8
+ * detached process that outlives the gateway that asked for it: the caller
9
+ * writes an intent file, spawns the installer, and then exits (gracefully, via
10
+ * the ShutdownCoordinator, on the console route).
11
+ *
12
+ * The status file is the contract between the two processes: the gateway writes
13
+ * `{status:"installing"}` before spawning, and the detached installer
14
+ * (spawnDetachedInstaller's wrapper) overwrites it with
15
+ * `{status:"success"|"failed", exitCode, stderrTail}` when npm finishes.
16
+ * `GET /updates/status` reads it back, so the dashboard knows whether the
17
+ * install landed or died — instead of polling a release tag forever with no
18
+ * answer when npm failed.
19
+ */
20
+ import { spawn } from "node:child_process";
21
+ import { closeSync, mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs";
22
+ import { join } from "node:path";
23
+ import { resolveUserConfigDir } from "../../config/package-paths";
24
+
25
+ /** Shape written before the installer spawns and overwritten by its wrapper. */
26
+ export interface UpdateStatus {
27
+ readonly status: "installing" | "success" | "failed";
28
+ readonly targetTag?: string;
29
+ readonly startedAt: string;
30
+ readonly exitCode?: number;
31
+ /** Last bytes of the installer's stderr, so a failed install is diagnosable. */
32
+ readonly stderrTail?: string;
33
+ }
34
+
35
+ /** Age under which a fresh `installing` status means "already running". */
36
+ export const INSTALLING_FRESH_MS = 10 * 60_000;
37
+
38
+ const UPDATE_STATUS_FILE = "update-status.json";
39
+ const UPDATE_LOG_FILE = "update.log";
40
+
41
+ /** How much of the installer's stderr is preserved in a failed status. */
42
+ const STDERR_TAIL_BYTES = 4_096;
43
+
44
+ export function updateStatusPath(configDir: string = resolveUserConfigDir()): string {
45
+ return join(configDir, UPDATE_STATUS_FILE);
46
+ }
47
+
48
+ export function updateLogPath(configDir: string = resolveUserConfigDir()): string {
49
+ return join(configDir, UPDATE_LOG_FILE);
50
+ }
51
+
52
+ /** Reads and parses the status file; any absence or corruption answers null. */
53
+ export function readUpdateStatus(configDir: string = resolveUserConfigDir()): UpdateStatus | null {
54
+ try {
55
+ const raw = readFileSync(updateStatusPath(configDir), "utf8");
56
+ const parsed = JSON.parse(raw) as Partial<UpdateStatus> | null;
57
+ if (
58
+ parsed === null ||
59
+ typeof parsed !== "object" ||
60
+ (parsed.status !== "installing" && parsed.status !== "success" && parsed.status !== "failed") ||
61
+ typeof parsed.startedAt !== "string"
62
+ ) {
63
+ return null;
64
+ }
65
+ return parsed as UpdateStatus;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /** Writes a status atomically enough for this purpose: one rename via write. */
72
+ export function writeUpdateStatus(status: UpdateStatus, configDir: string = resolveUserConfigDir()): void {
73
+ mkdirSync(configDir, { recursive: true });
74
+ writeFileSync(updateStatusPath(configDir), JSON.stringify(status));
75
+ }
76
+
77
+ /** True when a still-`installing` status was written recently enough to trust. */
78
+ export function isFreshlyInstalling(status: UpdateStatus | null, now: number = Date.now()): boolean {
79
+ if (status === null || status.status !== "installing") return false;
80
+ const startedAt = Date.parse(status.startedAt);
81
+ if (!Number.isFinite(startedAt)) return false;
82
+ return now - startedAt < INSTALLING_FRESH_MS;
83
+ }
84
+
85
+ /**
86
+ * Processes still running out of the package directory, so npm can rename it.
87
+ *
88
+ * The rename is the whole install on Windows, and it fails with EBUSY while
89
+ * anything holds the directory — including a hold that outlives the process
90
+ * that took it. `carte` itself runs from that directory, so the install has to
91
+ * wait until this process is gone; these are the others.
92
+ */
93
+ export function stopHoldingProcesses(): void {
94
+ if (process.platform !== "win32") return;
95
+ // `taskkill` filters on the window title and image name only, so it is asked
96
+ // for the two processes this CLI can leave behind. /T takes the tree, so a
97
+ // detached gateway started by a tray goes with it.
98
+ for (const image of ["cartethyia.exe", "carte.exe"]) {
99
+ try {
100
+ spawn("taskkill", ["/F", "/T", "/IM", image], { stdio: "ignore" }).unref();
101
+ } catch {
102
+ // A missing taskkill is not the install's problem; npm will report the
103
+ // hold if one is left.
104
+ }
105
+ }
106
+ }
107
+
108
+ export const UPDATE_INSTALL_COMMAND = "npm install -g cartethyia@latest --prefer-online";
109
+
110
+ /**
111
+ * Spawns the detached installer and returns immediately.
112
+ *
113
+ * `stdio` is redirected to `update.log` (append) rather than ignored, so a
114
+ * failed npm run leaves its output behind for the operator. The spawned
115
+ * wrapper re-execs this module's finish step as a `bun -e` script that runs
116
+ * `npm` to completion and then overwrites the status file — see
117
+ * {@link finishDetachedUpdate}, the function the wrapper string mirrors. If the
118
+ * wrapper itself cannot be spawned, the status is flipped to `failed` here so
119
+ * the dashboard's poll loop is not left spinning on a silent `installing`.
120
+ */
121
+ export function spawnDetachedUpdate(options: {
122
+ readonly targetTag?: string;
123
+ readonly configDir?: string;
124
+ }): { started: boolean; error?: string } {
125
+ const configDir = options.configDir ?? resolveUserConfigDir();
126
+ const isWin = process.platform === "win32";
127
+ const restart = isWin
128
+ ? "cartethyia start"
129
+ : "systemctl restart cartethyia 2>/dev/null || cartethyia start";
130
+ const script = `${UPDATE_INSTALL_COMMAND} && ${restart}`;
131
+ const wrapper = buildInstallerWrapperScript(script);
132
+ try {
133
+ stopHoldingProcesses();
134
+ let logFd: number | undefined;
135
+ try {
136
+ mkdirSync(configDir, { recursive: true });
137
+ logFd = openSync(updateLogPath(configDir), "a");
138
+ } catch {
139
+ // A log file that cannot be opened must not block the install; the
140
+ // installer then runs with stdio ignored as before.
141
+ }
142
+ const child = spawn(
143
+ process.execPath,
144
+ ["-e", wrapper],
145
+ { detached: true, stdio: logFd === undefined ? "ignore" : ["ignore", logFd, logFd] },
146
+ );
147
+ child.unref();
148
+ if (logFd !== undefined) {
149
+ try {
150
+ // The parent's copy of the fd is not needed once the child inherits it.
151
+ closeSync(logFd);
152
+ } catch {
153
+ // An unclosed fd in a process that is about to drain and exit is
154
+ // harmless.
155
+ }
156
+ }
157
+ return { started: true };
158
+ } catch (error) {
159
+ const message = error instanceof Error ? error.message : String(error);
160
+ try {
161
+ writeUpdateStatus(
162
+ { status: "failed", ...(options.targetTag === undefined ? {} : { targetTag: options.targetTag }), startedAt: new Date().toISOString(), stderrTail: message },
163
+ configDir,
164
+ );
165
+ } catch {
166
+ // Nothing more can be reported if even the status write fails.
167
+ }
168
+ return { started: false, error: message };
169
+ }
170
+ }
171
+
172
+ /**
173
+ * The script string handed to `bun -e` (or `node -e`, whatever `process.execPath`
174
+ * is). It mirrors {@link finishDetachedUpdate} because a detached process cannot
175
+ * import this module: it must be one self-contained expression that survives the
176
+ * parent exiting. Kept in this file, next to the function, so the two are
177
+ * reviewed together.
178
+ */
179
+ function buildInstallerWrapperScript(script: string): string {
180
+ const installer = JSON.stringify(script);
181
+ const shell = JSON.stringify(process.platform === "win32" ? "cmd.exe" : "/bin/sh");
182
+ const argsPrefix = process.platform === "win32" ? '"/c",' : '"-c",';
183
+ const statusPath = JSON.stringify(updateStatusPath());
184
+ return `
185
+ const { spawn } = require("node:child_process");
186
+ const { readFileSync, writeFileSync, openSync } = require("node:fs");
187
+ const startedAt = new Date().toISOString();
188
+ let logFd;
189
+ try { logFd = openSync(${JSON.stringify(updateLogPath())}, "a"); } catch {}
190
+ const child = spawn(${shell}, [${argsPrefix} ${installer}], { stdio: logFd === undefined ? "ignore" : ["ignore", logFd, logFd] });
191
+ let stderrTail = "";
192
+ child.on("exit", (exitCode) => {
193
+ writeFileSync(${statusPath}, JSON.stringify({
194
+ status: exitCode === 0 ? "success" : "failed",
195
+ startedAt,
196
+ exitCode: exitCode ?? -1,
197
+ ...(stderrTail ? { stderrTail } : {}),
198
+ }));
199
+ process.exit(exitCode ?? -1);
200
+ });
201
+ child.on("error", (error) => {
202
+ try {
203
+ writeFileSync(${statusPath}, JSON.stringify({ status: "failed", startedAt, exitCode: -1, stderrTail: String(error) }));
204
+ } catch {}
205
+ process.exit(-1);
206
+ });
207
+ `.trim();
208
+ }
209
+
210
+ /**
211
+ * Runs the install to completion and records the terminal status. Exported for
212
+ * the test suite, which exercises the same code shape the wrapper string
213
+ * executes; production reaches it only through the detached wrapper.
214
+ */
215
+ export async function finishDetachedUpdate(
216
+ runInstall: (onStderr: (chunk: string) => void) => Promise<number>,
217
+ configDir: string = resolveUserConfigDir(),
218
+ ): Promise<UpdateStatus> {
219
+ const startedAt = new Date().toISOString();
220
+ let stderrTail = "";
221
+ let exitCode: number;
222
+ try {
223
+ exitCode = await runInstall((chunk) => {
224
+ stderrTail = (stderrTail + chunk).slice(-STDERR_TAIL_BYTES);
225
+ });
226
+ } catch (error) {
227
+ const status: UpdateStatus = {
228
+ status: "failed",
229
+ startedAt,
230
+ exitCode: -1,
231
+ stderrTail: (error instanceof Error ? error.message : String(error)).slice(-STDERR_TAIL_BYTES),
232
+ };
233
+ writeUpdateStatus(status, configDir);
234
+ return status;
235
+ }
236
+ const status: UpdateStatus = {
237
+ status: exitCode === 0 ? "success" : "failed",
238
+ startedAt,
239
+ exitCode,
240
+ ...(stderrTail ? { stderrTail } : {}),
241
+ };
242
+ writeUpdateStatus(status, configDir);
243
+ return status;
244
+ }
@@ -1,15 +1,29 @@
1
1
  import { Elysia, type AnyElysia } from "elysia";
2
- import { spawn } from "node:child_process";
3
- import { consoleErrorHandler, requireTenantScope } from "../shared/errors";
2
+ import { consoleErrorHandler, requireTenantScope, ConsoleDomainError } from "../shared/errors";
4
3
  import { consoleSseResponse, createConsoleSseStream } from "../observability/sse";
5
4
  import type { ObservabilityStore } from "../observability/contracts";
6
5
  import type { ConsoleAccessResolver } from "../auth/access";
7
6
  import { fetchLatestRelease, type LatestRelease } from "./repository-updates";
8
7
  import { isNewerRelease } from "./version-compare";
8
+ import {
9
+ isFreshlyInstalling,
10
+ readUpdateStatus,
11
+ spawnDetachedUpdate,
12
+ updateStatusPath,
13
+ writeUpdateStatus,
14
+ } from "./apply-update";
15
+ import { CARTETHYIA_VERSION } from "../../version";
9
16
 
10
- // Memory is sampled per visible tab: ten seconds per stream made a console
11
- // with many open tabs wake up constantly for numbers nobody is reading. 30 s
12
- // is still responsive for a memory warning while cutting that work by three.
17
+ /** Minimal drain surface the apply route needs from the process coordinator. */
18
+ export interface UpdateShutdownCoordinator {
19
+ readonly begin: (reason: "update") => Promise<void>;
20
+ readonly totalShutdownBudgetMs: () => number;
21
+ }
22
+
23
+ // Memory is sampled once per process (a single shared sampler feeding every
24
+ // notifications subscriber), not per visible tab: ten seconds per stream made a
25
+ // console with many open tabs wake up constantly for numbers nobody is reading.
26
+ // 30 s is still responsive for a memory warning while cutting that work by three.
13
27
  const MEMORY_SAMPLE_INTERVAL_MS = 30_000;
14
28
  const UPDATE_SAMPLE_INTERVAL_MS = 60_000;
15
29
  const HIGH_MEMORY_PERCENT = 85;
@@ -30,7 +44,7 @@ const MEMORY_GROWTH_RATIO = 1.2;
30
44
  const MAX_MEMORY_SAMPLES = 8;
31
45
  const RELEASES_URL = "https://github.com/risunCodex/Cartethyia/releases";
32
46
 
33
- export interface NotificationEvent {
47
+ interface NotificationEvent {
34
48
  readonly id: string;
35
49
  readonly kind: "memory-high" | "memory-growth" | "update-available";
36
50
  readonly title: string;
@@ -38,6 +52,8 @@ export interface NotificationEvent {
38
52
  readonly createdAt: string;
39
53
  readonly level: "warning" | "info";
40
54
  readonly href?: string;
55
+ /** The release tag that triggered an `update-available` alert (with `v` prefix). */
56
+ readonly version?: string;
41
57
  }
42
58
 
43
59
  export function memoryNotifications(
@@ -85,6 +101,143 @@ export function memoryNotifications(
85
101
  };
86
102
  }
87
103
 
104
+ /** A subscriber of the process-wide update/notification sampler. */
105
+ type NotificationListener = (notification: NotificationEvent) => void;
106
+
107
+ /**
108
+ * One sampler per gateway composition (the console mounts `createUpdateRoutes`
109
+ * exactly once per process, so this is process-scoped in production). It owns
110
+ * the memory and release cadences and fans results out to every subscriber;
111
+ * an SSE connection only subscribes. The timer runs while at least one
112
+ * subscriber is connected and stops with the last one, so an idle gateway
113
+ * samples nothing.
114
+ */
115
+ function createUpdateSampler(config: {
116
+ readonly observabilityStore: ObservabilityStore;
117
+ readonly fetchRelease: () => Promise<LatestRelease | null>;
118
+ }): { subscribe(listener: NotificationListener): () => void } {
119
+ const subscribers = new Set<NotificationListener>();
120
+ const samples: Array<{ at: number; rss: number; percent: number }> = [];
121
+ let memoryState = { high: false, growth: false };
122
+ let latestTag: string | undefined;
123
+ let memoryTimer: ReturnType<typeof setTimeout> | undefined;
124
+ let updateTimer: ReturnType<typeof setTimeout> | undefined;
125
+
126
+ const broadcast = (notifications: readonly NotificationEvent[]): void => {
127
+ if (notifications.length === 0) return;
128
+ for (const listener of subscribers) {
129
+ for (const notification of notifications) listener(notification);
130
+ }
131
+ };
132
+
133
+ const sampleMemory = async (): Promise<void> => {
134
+ try {
135
+ const at = Date.now();
136
+ // Fresh, not `health`: that read is cached for 10 s and this samples on
137
+ // that exact cadence, so the cached bytes would make consecutive samples
138
+ // identical and a growth trend unreadable.
139
+ const live = await config.observabilityStore.liveMemory();
140
+ samples.push({ at, rss: live.memory_bytes, percent: live.memory_percent });
141
+ while (samples.length > 0 && at - samples[0]!.at > MEMORY_GROWTH_WINDOW_MS * 2) samples.shift();
142
+ if (samples.length > MAX_MEMORY_SAMPLES) samples.splice(0, samples.length - MAX_MEMORY_SAMPLES);
143
+ const computed = memoryNotifications(samples, memoryState);
144
+ memoryState = { high: computed.high, growth: computed.growth };
145
+ broadcast(computed.notifications);
146
+ } catch {
147
+ // A transient check must not terminate the feed or hide later events.
148
+ }
149
+ };
150
+
151
+ const sampleUpdate = async (): Promise<void> => {
152
+ try {
153
+ const release = await config.fetchRelease();
154
+ if (release === null || release.tag_name === latestTag) return;
155
+ if (!isNewerRelease(CARTETHYIA_VERSION, release.tag_name)) return;
156
+ latestTag = release.tag_name;
157
+ broadcast([{
158
+ id: `update-${release.tag_name}`,
159
+ kind: "update-available",
160
+ title: "Cartethyia update available",
161
+ message: `Version ${release.tag_name} is available; this gateway runs ${CARTETHYIA_VERSION}.`,
162
+ createdAt: new Date().toISOString(),
163
+ level: "info",
164
+ href: RELEASES_URL,
165
+ version: release.tag_name,
166
+ } satisfies NotificationEvent]);
167
+ } catch {
168
+ // Release discovery is best-effort and cannot interrupt memory sampling.
169
+ }
170
+ };
171
+
172
+ const start = (): void => {
173
+ if (memoryTimer !== undefined) return;
174
+ memoryTimer = setTimeout(async function tick() {
175
+ memoryTimer = setTimeout(tick, MEMORY_SAMPLE_INTERVAL_MS);
176
+ await sampleMemory();
177
+ }, MEMORY_SAMPLE_INTERVAL_MS);
178
+ updateTimer = setTimeout(async function tick() {
179
+ updateTimer = setTimeout(tick, UPDATE_SAMPLE_INTERVAL_MS);
180
+ await sampleUpdate();
181
+ }, UPDATE_SAMPLE_INTERVAL_MS);
182
+ };
183
+
184
+ const stop = (): void => {
185
+ if (memoryTimer !== undefined) clearTimeout(memoryTimer);
186
+ if (updateTimer !== undefined) clearTimeout(updateTimer);
187
+ memoryTimer = undefined;
188
+ updateTimer = undefined;
189
+ };
190
+
191
+ return {
192
+ subscribe(listener) {
193
+ subscribers.add(listener);
194
+ // Bootstrap: the first frames a fresh connection sees are the current
195
+ // alerts — a memory state that is high right now, an update that exists
196
+ // now — computed with a fresh transition state exactly like the old
197
+ // per-connection initial tick did. After that, the shared cadence takes
198
+ // over.
199
+ void (async () => {
200
+ try {
201
+ const at = Date.now();
202
+ const live = await config.observabilityStore.liveMemory();
203
+ const ring = [
204
+ ...samples,
205
+ { at, rss: live.memory_bytes, percent: live.memory_percent },
206
+ ].filter((sample) => at - sample.at <= MEMORY_GROWTH_WINDOW_MS * 2)
207
+ .slice(-MAX_MEMORY_SAMPLES);
208
+ for (const notification of memoryNotifications(ring, { high: false, growth: false }).notifications) {
209
+ listener(notification);
210
+ }
211
+ } catch {
212
+ // A transient read must not fail the subscription.
213
+ }
214
+ try {
215
+ const release = await config.fetchRelease();
216
+ if (release !== null && isNewerRelease(CARTETHYIA_VERSION, release.tag_name)) {
217
+ listener({
218
+ id: `update-${release.tag_name}`,
219
+ kind: "update-available",
220
+ title: "Cartethyia update available",
221
+ message: `Version ${release.tag_name} is available; this gateway runs ${CARTETHYIA_VERSION}.`,
222
+ createdAt: new Date().toISOString(),
223
+ level: "info",
224
+ href: RELEASES_URL,
225
+ version: release.tag_name,
226
+ } satisfies NotificationEvent);
227
+ }
228
+ } catch {
229
+ // Best-effort release discovery.
230
+ }
231
+ })();
232
+ if (subscribers.size > 0) start();
233
+ return () => {
234
+ subscribers.delete(listener);
235
+ if (subscribers.size === 0) stop();
236
+ };
237
+ },
238
+ };
239
+ }
240
+
88
241
  /**
89
242
  * Read-only update discovery for the console sidebar.
90
243
  *
@@ -97,109 +250,111 @@ export function createUpdateRoutes(config: {
97
250
  readonly accessResolver: ConsoleAccessResolver;
98
251
  readonly observabilityStore: ObservabilityStore;
99
252
  readonly fetchRelease?: () => Promise<LatestRelease | null>;
253
+ /**
254
+ * The process `ShutdownCoordinator`, narrowed to the drain surface the apply
255
+ * route needs. Optional only so reduced compositions (tests of the read-only
256
+ * routes) build without one; the apply route itself throws when absent,
257
+ * because exiting without a drain is the bug this redesign removes.
258
+ */
259
+ readonly shutdownCoordinator?: UpdateShutdownCoordinator;
260
+ /** Overrides status-file resolution (tests); defaults to the config dir. */
261
+ readonly configDir?: string;
262
+ /** Overrides the detached spawn (tests); defaults to the real installer. */
263
+ readonly spawnUpdate?: typeof spawnDetachedUpdate;
100
264
  }): AnyElysia {
101
265
  const fetchRelease = config.fetchRelease ?? fetchLatestRelease;
266
+ const configDir = config.configDir;
267
+ const spawnUpdate = config.spawnUpdate ?? spawnDetachedUpdate;
268
+ const sampler = createUpdateSampler({
269
+ observabilityStore: config.observabilityStore,
270
+ fetchRelease,
271
+ });
102
272
  return new Elysia()
103
273
  .error(consoleErrorHandler("Update check failed"))
104
274
  .get("/updates/release", async () => await fetchRelease())
105
- .post("/updates/apply", ({ request }) => {
275
+ // Same no-scope posture as `/updates/release`: the status file holds no
276
+ // credentials and no tenant data, only whether the last install landed.
277
+ .get("/updates/status", () => readUpdateStatus(configDir) ?? { status: "idle", startedAt: new Date(0).toISOString() })
278
+ .post("/updates/apply", async ({ request }) => {
106
279
  requireTenantScope(config.accessResolver(request), "dashboard:write");
107
- const isWin = process.platform === "win32";
108
- const shell = isWin ? "cmd.exe" : "/bin/sh";
109
- const install = "npm install -g cartethyia@latest --prefer-online";
110
- const restart = isWin
111
- ? "cartethyia start"
112
- : "systemctl restart cartethyia 2>/dev/null || cartethyia start";
113
- const script = `${install} && ${restart}`;
114
- const args = isWin ? ["/c", script] : ["-c", script];
115
- const child = spawn(shell, args, { detached: true, stdio: "ignore" });
116
- child.unref();
117
- setTimeout(() => process.exit(0), 1500);
118
- return { started: true };
280
+ if (config.shutdownCoordinator === undefined) {
281
+ throw new ConsoleDomainError(
282
+ "update_unavailable",
283
+ 503,
284
+ "The update flow needs a shutdown coordinator; this composition cannot apply updates.",
285
+ );
286
+ }
287
+ const status = readUpdateStatus(configDir);
288
+ // Double-apply guard: a status file younger than ten minutes that still
289
+ // says "installing" means the detached installer is running and another
290
+ // spawn would race it for the same npm cache and package directory.
291
+ if (isFreshlyInstalling(status)) {
292
+ throw new ConsoleDomainError(
293
+ "update_in_progress",
294
+ 409,
295
+ "An update is already installing. Check /updates/status for its result.",
296
+ );
297
+ }
298
+ // The target tag is best-effort: it labels the intent file, but a failed
299
+ // release lookup must not block the install itself.
300
+ const release = await fetchRelease().catch(() => null);
301
+ const targetTag = release?.tag_name.replace(/^v/, "");
302
+ writeUpdateStatus(
303
+ {
304
+ status: "installing",
305
+ ...(targetTag === undefined ? {} : { targetTag }),
306
+ startedAt: new Date().toISOString(),
307
+ },
308
+ configDir,
309
+ );
310
+ const spawnResult = spawnUpdate(configDir === undefined ? {} : { configDir });
311
+ if (!spawnResult.started) {
312
+ // Flip the intent file to `failed` here, regardless of whether the spawn
313
+ // implementation already did: the dashboard's poll loop must see a
314
+ // terminal status, never a silent `installing` with no installer behind it.
315
+ writeUpdateStatus(
316
+ {
317
+ status: "failed",
318
+ startedAt: new Date().toISOString(),
319
+ exitCode: -1,
320
+ stderrTail: spawnResult.error ?? "unknown error",
321
+ },
322
+ configDir,
323
+ );
324
+ throw new ConsoleDomainError(
325
+ "update_spawn_failed",
326
+ 500,
327
+ "The detached installer could not be started.",
328
+ { error: spawnResult.error ?? "unknown" },
329
+ );
330
+ }
331
+ const coordinator = config.shutdownCoordinator;
332
+ // Drain like a planned stop, then exit. The safety timer is the
333
+ // coordinator's own total budget: begin() is bounded by design, but a
334
+ // hung hook must not keep a half-updated process alive past it.
335
+ const budgetMs = coordinator.totalShutdownBudgetMs();
336
+ const safety = setTimeout(() => process.exit(0), budgetMs);
337
+ if (typeof safety.unref === "function") safety.unref();
338
+ void coordinator
339
+ .begin("update")
340
+ .catch(() => undefined)
341
+ .finally(() => {
342
+ clearTimeout(safety);
343
+ process.exit(0);
344
+ })
345
+ // The finally callback itself must never surface as an unhandled
346
+ // rejection: exit either succeeded or the process is already leaving.
347
+ .catch(() => undefined);
348
+ return { started: true, statusPath: updateStatusPath(configDir) };
119
349
  })
120
350
  .get("/notifications/stream", ({ request }) => {
121
- const tenantId = requireTenantScope(
351
+ requireTenantScope(
122
352
  config.accessResolver(request),
123
353
  "dashboard:read",
124
- ).tenantId;
354
+ );
125
355
  return consoleSseResponse(
126
356
  createConsoleSseStream(request.signal, (sender) => {
127
- let closed = false;
128
- let memoryTimer: ReturnType<typeof setTimeout> | undefined;
129
- let updateTimer: ReturnType<typeof setTimeout> | undefined;
130
- let latestTag: string | undefined;
131
- let memoryState = { high: false, growth: false };
132
- let samples: Array<{ at: number; rss: number; percent: number }> = [];
133
-
134
- const publishMemory = async (): Promise<void> => {
135
- if (closed) return;
136
- try {
137
- const at = Date.now();
138
- // Fresh, not `health`: that read is cached for 10 s and this
139
- // samples on that exact cadence, so the cached bytes would make
140
- // consecutive samples identical and a growth trend unreadable.
141
- const live = await config.observabilityStore.liveMemory();
142
- samples = [
143
- ...samples,
144
- { at, rss: live.memory_bytes, percent: live.memory_percent },
145
- ].filter((sample) => at - sample.at <= MEMORY_GROWTH_WINDOW_MS * 2)
146
- .slice(-MAX_MEMORY_SAMPLES);
147
- const computed = memoryNotifications(samples, memoryState);
148
- memoryState = { high: computed.high, growth: computed.growth };
149
- for (const notification of computed.notifications) sender.send("notification", notification);
150
- } catch {
151
- // A transient check must not terminate the feed or hide later events.
152
- }
153
- };
154
-
155
- const publishUpdate = async (): Promise<void> => {
156
- if (closed) return;
157
- try {
158
- const release = await fetchRelease();
159
- if (release === null || release.tag_name === latestTag) return;
160
- const health = (await config.observabilityStore.health(tenantId)).value;
161
- if (!isNewerRelease(health.version, release.tag_name)) return;
162
- latestTag = release.tag_name;
163
- sender.send("notification", {
164
- id: `update-${release.tag_name}`,
165
- kind: "update-available",
166
- title: "Cartethyia update available",
167
- message: `Version ${release.tag_name} is available; this gateway runs ${health.version}.`,
168
- createdAt: new Date().toISOString(),
169
- level: "info",
170
- href: RELEASES_URL,
171
- } satisfies NotificationEvent);
172
- } catch {
173
- // Release discovery is best-effort and cannot interrupt memory sampling.
174
- }
175
- };
176
-
177
- const scheduleMemory = (): void => {
178
- memoryTimer = setTimeout(() => {
179
- void publishMemory().finally(() => {
180
- if (!closed) scheduleMemory();
181
- });
182
- }, MEMORY_SAMPLE_INTERVAL_MS);
183
- };
184
- const scheduleUpdate = (): void => {
185
- updateTimer = setTimeout(() => {
186
- void publishUpdate().finally(() => {
187
- if (!closed) scheduleUpdate();
188
- });
189
- }, UPDATE_SAMPLE_INTERVAL_MS);
190
- };
191
-
192
- void publishMemory().finally(() => {
193
- if (!closed) scheduleMemory();
194
- });
195
- void publishUpdate().finally(() => {
196
- if (!closed) scheduleUpdate();
197
- });
198
- return () => {
199
- closed = true;
200
- clearTimeout(memoryTimer);
201
- clearTimeout(updateTimer);
202
- };
357
+ return sampler.subscribe((notification) => sender.send("notification", notification));
203
358
  }),
204
359
  );
205
360
  });