cartethyia 2.5.2 → 2.5.4

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 (277) hide show
  1. package/dist/dashboard/assets/{BannedPage-MsQaeQTs.js → BannedPage-C2orBsI2.js} +1 -1
  2. package/dist/dashboard/assets/BannedPage-C2orBsI2.js.gz +0 -0
  3. package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js +1 -0
  4. package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolsPage-DfWHTJ7v.js → CliToolsPage-BV6pxVQo.js} +1 -1
  6. package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
  7. package/dist/dashboard/assets/{CombosPage-BrCBQDAc.js → CombosPage-Bsan4679.js} +1 -1
  8. package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
  9. package/dist/dashboard/assets/{ConsoleLogPage-C6Uh33er.js → ConsoleLogPage-CW-wPmJ6.js} +1 -1
  10. package/dist/dashboard/assets/ConsoleLogPage-CW-wPmJ6.js.gz +0 -0
  11. package/dist/dashboard/assets/{CustomizationPage-8aS9hyJu.js → CustomizationPage-0p8uX7vB.js} +1 -1
  12. package/dist/dashboard/assets/CustomizationPage-0p8uX7vB.js.gz +0 -0
  13. package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js +1 -0
  14. package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js.gz +0 -0
  15. package/dist/dashboard/assets/{LoginPage-C8hKyXBe.js → LoginPage-BvzCx8Kr.js} +1 -1
  16. package/dist/dashboard/assets/LoginPage-BvzCx8Kr.js.gz +0 -0
  17. package/dist/dashboard/assets/{ModelPicker-CWkTUtpX.js → ModelPicker-BY1aKEkP.js} +1 -1
  18. package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js.gz +0 -0
  19. package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js +1 -0
  20. package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js.gz +0 -0
  21. package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js +3 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js.gz +0 -0
  23. package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js +1 -0
  24. package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js.gz +0 -0
  25. package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js +7 -0
  26. package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js.gz +0 -0
  27. package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js +1 -0
  28. package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js.gz +0 -0
  29. package/dist/dashboard/assets/SettingsPage-CS86ho5X.js +1 -0
  30. package/dist/dashboard/assets/SettingsPage-CS86ho5X.js.gz +0 -0
  31. package/dist/dashboard/assets/{SetupPage-CXjUlfqr.js → SetupPage-DDbMQsoo.js} +1 -1
  32. package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
  33. package/dist/dashboard/assets/{SortableList-C-_oD3F2.js → SortableList-CB3Z7VTf.js} +1 -1
  34. package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
  35. package/dist/dashboard/assets/{StudioPage-u9IxmYOI.js → StudioPage-D95UCPHI.js} +3 -3
  36. package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
  37. package/dist/dashboard/assets/{UsagePage-BEVLVrwP.js → UsagePage-Cnae9SWA.js} +13 -13
  38. package/dist/dashboard/assets/UsagePage-Cnae9SWA.js.gz +0 -0
  39. package/dist/dashboard/assets/{api-keys-uNywYlfv.js → api-keys-VY68Mhd1.js} +1 -1
  40. package/dist/dashboard/assets/api-keys-VY68Mhd1.js.gz +0 -0
  41. package/dist/dashboard/assets/app-COlK95oi.css +2 -0
  42. package/dist/dashboard/assets/app-COlK95oi.css.gz +0 -0
  43. package/dist/dashboard/assets/{assert-NHwYq95t.js → assert-Chi7pCnt.js} +1 -1
  44. package/dist/dashboard/assets/assert-Chi7pCnt.js.gz +0 -0
  45. package/dist/dashboard/assets/{badge-6uzDDvqs.js → badge-D4pQapKj.js} +1 -1
  46. package/dist/dashboard/assets/badge-D4pQapKj.js.gz +0 -0
  47. package/dist/dashboard/assets/{banner-4_WNg2WZ.js → banner-DH6cg-_B.js} +1 -1
  48. package/dist/dashboard/assets/banner-DH6cg-_B.js.gz +0 -0
  49. package/dist/dashboard/assets/{card--AtFedAq.js → card-BKqlUOe4.js} +1 -1
  50. package/dist/dashboard/assets/card-BKqlUOe4.js.gz +0 -0
  51. package/dist/dashboard/assets/{cli-tools-B3e7dx3N.js → cli-tools-fTgljTGA.js} +1 -1
  52. package/dist/dashboard/assets/cli-tools-fTgljTGA.js.gz +0 -0
  53. package/dist/dashboard/assets/{clipboard-button-NreUx6c8.js → clipboard-button-D5Ys_sM0.js} +1 -1
  54. package/dist/dashboard/assets/clipboard-button-D5Ys_sM0.js.gz +0 -0
  55. package/dist/dashboard/assets/database-Dz9N3zGd.js +1 -0
  56. package/dist/dashboard/assets/database-Dz9N3zGd.js.gz +0 -0
  57. package/dist/dashboard/assets/{dialog-BtpsJU98.js → dialog-BYj-8WuK.js} +1 -1
  58. package/dist/dashboard/assets/dialog-BYj-8WuK.js.gz +0 -0
  59. package/dist/dashboard/assets/{drawer-WwwtzihF.js → drawer-CmXIZ6H1.js} +1 -1
  60. package/dist/dashboard/assets/drawer-CmXIZ6H1.js.gz +0 -0
  61. package/dist/dashboard/assets/entry-CBg5zeu8.js +1 -0
  62. package/dist/dashboard/assets/entry-CBg5zeu8.js.gz +0 -0
  63. package/dist/dashboard/assets/entry-CQBr6U9c.js +1 -0
  64. package/dist/dashboard/assets/entry-CQBr6U9c.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-CgdSAzEV.js +4 -0
  66. package/dist/dashboard/assets/entry-CgdSAzEV.js.gz +0 -0
  67. package/dist/dashboard/assets/{github-badge-D_eM3IgG.js → github-badge-BlptL8Uf.js} +1 -1
  68. package/dist/dashboard/assets/github-badge-BlptL8Uf.js.gz +0 -0
  69. package/dist/dashboard/assets/{index-DCBtROoK.js → index-js6TJvdr.js} +2 -2
  70. package/dist/dashboard/assets/index-js6TJvdr.js.gz +0 -0
  71. package/dist/dashboard/assets/{inline-BcNGF8Gr.js → inline-DUWDSmDV.js} +1 -1
  72. package/dist/dashboard/assets/inline-DUWDSmDV.js.gz +0 -0
  73. package/dist/dashboard/assets/{input-BG41et9G.js → input-LVjgZq4f.js} +1 -1
  74. package/dist/dashboard/assets/input-LVjgZq4f.js.gz +0 -0
  75. package/dist/dashboard/assets/{layout-C8mer_Ak.js → layout-CPfxpsfy.js} +1 -1
  76. package/dist/dashboard/assets/layout-CPfxpsfy.js.gz +0 -0
  77. package/dist/dashboard/assets/{list-row-Gh_W4OR_.js → list-row-DupvLfDx.js} +1 -1
  78. package/dist/dashboard/assets/list-row-DupvLfDx.js.gz +0 -0
  79. package/dist/dashboard/assets/{live-BJGYRBnR.js → live-C4pEJUqI.js} +1 -1
  80. package/dist/dashboard/assets/live-C4pEJUqI.js.gz +0 -0
  81. package/dist/dashboard/assets/model-bans-BRiuufkB.js +1 -0
  82. package/dist/dashboard/assets/model-bans-BRiuufkB.js.gz +0 -0
  83. package/dist/dashboard/assets/{mutation-BkoLM5Ba.js → mutation-B4ROwk8L.js} +1 -1
  84. package/dist/dashboard/assets/mutation-B4ROwk8L.js.gz +0 -0
  85. package/dist/dashboard/assets/numbers-YNcHvRY7.js +2 -0
  86. package/dist/dashboard/assets/numbers-YNcHvRY7.js.gz +0 -0
  87. package/dist/dashboard/assets/{query-keys-D7wX2vM5.js → query-keys-gmjzC3RS.js} +1 -1
  88. package/dist/dashboard/assets/{query-keys-D7wX2vM5.js.gz → query-keys-gmjzC3RS.js.gz} +0 -0
  89. package/dist/dashboard/assets/{routing-CcusqXA7.js → routing-C7tBT5o-.js} +1 -1
  90. package/dist/dashboard/assets/routing-C7tBT5o-.js.gz +0 -0
  91. package/dist/dashboard/assets/{select-DsX5QT8K.js → select-WUCXorXw.js} +2 -2
  92. package/dist/dashboard/assets/select-WUCXorXw.js.gz +0 -0
  93. package/dist/dashboard/assets/{settings-mpmXTgUa.js → settings-C_zKl1GS.js} +1 -1
  94. package/dist/dashboard/assets/settings-C_zKl1GS.js.gz +0 -0
  95. package/dist/dashboard/assets/{stack-DwLmjUFK.js → stack-_3O0F8Xn.js} +1 -1
  96. package/dist/dashboard/assets/stack-_3O0F8Xn.js.gz +0 -0
  97. package/dist/dashboard/assets/{state-DYdQmdAl.js → state-BgTxHcJS.js} +1 -1
  98. package/dist/dashboard/assets/state-BgTxHcJS.js.gz +0 -0
  99. package/dist/dashboard/assets/switch-DEjTtOWf.js +1 -0
  100. package/dist/dashboard/assets/switch-DEjTtOWf.js.gz +0 -0
  101. package/dist/dashboard/assets/terminal-0_IjPWTk.js +1 -0
  102. package/dist/dashboard/assets/terminal-0_IjPWTk.js.gz +0 -0
  103. package/dist/dashboard/assets/{useMutation-w4u4CVue.js → useMutation-BWSC0UAS.js} +1 -1
  104. package/dist/dashboard/assets/useMutation-BWSC0UAS.js.gz +0 -0
  105. package/dist/dashboard/assets/zap-C4EeVW4q.js +1 -0
  106. package/dist/dashboard/assets/zap-C4EeVW4q.js.gz +0 -0
  107. package/dist/dashboard/donation/qris-risun-marke.png +0 -0
  108. package/dist/dashboard/index.html +1 -1
  109. package/dist/dashboard/index.html.gz +0 -0
  110. package/dist/dashboard/precompressed-assets.json +54 -54
  111. package/package.json +1 -1
  112. package/scripts/cli/autostart.ts +237 -0
  113. package/scripts/cli/command/console-menu.ts +244 -246
  114. package/scripts/cli/command/tray.ts +208 -204
  115. package/scripts/cli/command/update.ts +27 -10
  116. package/scripts/cli/main.ts +28 -1
  117. package/src/boot/app.ts +693 -693
  118. package/src/boot/dependencies.ts +646 -616
  119. package/src/config/spec.ts +81 -2
  120. package/src/config/telemetry.ts +46 -0
  121. package/src/console/api-keys/bucket-spend.ts +15 -4
  122. package/src/console/backup/contracts.ts +15 -2
  123. package/src/console/backup/routes.ts +52 -1
  124. package/src/console/backup/service.ts +60 -4
  125. package/src/console/console-router.ts +8 -0
  126. package/src/console/data-management/contracts.ts +93 -0
  127. package/src/console/data-management/operations.ts +229 -0
  128. package/src/console/data-management/routes.ts +104 -0
  129. package/src/console/domain-registration.ts +39 -0
  130. package/src/console/observability/contracts.ts +524 -485
  131. package/src/console/observability/store.ts +1115 -1018
  132. package/src/console/providers/catalog/contracts.ts +6 -0
  133. package/src/console/providers/catalog/provider-operations.ts +20 -0
  134. package/src/console/providers/catalog/routes.ts +7 -0
  135. package/src/console/providers/catalog/store.ts +22 -1
  136. package/src/console/quota/account-quota-shared.ts +52 -39
  137. package/src/console/quota/refresh.ts +441 -429
  138. package/src/console/routing/pools/import-parse.ts +206 -0
  139. package/src/console/routing/pools/operations.ts +543 -501
  140. package/src/console/routing/pools/routes.ts +181 -164
  141. package/src/console/settings/contracts.ts +37 -0
  142. package/src/console/settings/store.ts +51 -0
  143. package/src/console/share/share-router.ts +6 -9
  144. package/src/console/updates/routes.ts +16 -0
  145. package/src/main.ts +10 -0
  146. package/src/observability/event-compaction.ts +315 -0
  147. package/src/observability/event-day-index.ts +442 -0
  148. package/src/observability/event-log.ts +152 -3
  149. package/src/observability/payload-capture.ts +373 -344
  150. package/src/observability/payload-store.ts +440 -366
  151. package/src/observability/telemetry-status.ts +28 -16
  152. package/src/persistence/health-event-flush.ts +36 -0
  153. package/src/persistence/schema.ts +1402 -1360
  154. package/src/persistence/stores/api-key-store.ts +6 -0
  155. package/src/persistence/stores/share-store.ts +583 -556
  156. package/src/persistence/stores/telemetry-store.ts +237 -214
  157. package/src/protocol/codecs.ts +1 -2
  158. package/src/protocol/image-clamp.ts +19 -1
  159. package/src/protocol/registry.ts +8 -0
  160. package/src/protocol/request/gemini.ts +9 -2
  161. package/src/protocol/request/messages.ts +14 -2
  162. package/src/protocol/response/gemini.ts +18 -4
  163. package/src/providers/adapters/compatible-adapter.ts +3 -0
  164. package/src/providers/bundled-discovery-providers.ts +122 -119
  165. package/src/providers/configured-openai-providers.ts +136 -134
  166. package/src/providers/default-registry.ts +5 -4
  167. package/src/providers/discovery/openai-model-discovery.ts +5 -0
  168. package/src/providers/discovery/openrouter-free-roster.ts +15 -17
  169. package/src/providers/integrations/antigravity/antigravity-quota.ts +11 -3
  170. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +4 -4
  171. package/src/providers/integrations/buddy/workbuddy/shared.ts +201 -200
  172. package/src/providers/integrations/cline/cline-quota.ts +132 -128
  173. package/src/providers/integrations/codex/codex-headers.ts +25 -39
  174. package/src/providers/integrations/codex/codex.ts +2 -29
  175. package/src/providers/integrations/cursor/cursor-quota.ts +310 -0
  176. package/src/providers/integrations/cursor/cursor-request.ts +616 -471
  177. package/src/providers/integrations/cursor/cursor-session-manager.ts +319 -281
  178. package/src/providers/integrations/cursor/cursor.ts +1485 -960
  179. package/src/providers/integrations/cursor/protocol/messages.ts +25 -0
  180. package/src/providers/integrations/factory/factory.ts +8 -1
  181. package/src/providers/integrations/gemini.ts +6 -1
  182. package/src/providers/integrations/grok/grok-quota.ts +279 -177
  183. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -0
  184. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -0
  185. package/src/providers/integrations/opencode/provider.ts +531 -532
  186. package/src/providers/integrations/qoder/qoder-quota.ts +192 -0
  187. package/src/providers/integrations/tokenharbor.ts +2 -2
  188. package/src/providers/integrations/trae/trae-catalog.ts +5 -3
  189. package/src/providers/integrations/trae/trae-quota.ts +384 -0
  190. package/src/providers/operations/account-health-service.ts +1523 -1457
  191. package/src/providers/operations/client-versions.ts +9 -2
  192. package/src/providers/provider-contracts.ts +7 -0
  193. package/src/providers/provider-metadata.ts +13 -1
  194. package/src/providers/quota/quota-support.ts +8 -0
  195. package/src/security/api-key-auth.ts +504 -494
  196. package/src/transport/dispatch/attempt-finalize.ts +46 -5
  197. package/src/transport/dispatch/generation.ts +6 -0
  198. package/src/transport/dispatch/proxy-request.ts +33 -1
  199. package/src/transport/dispatch/ratelimit-capture.ts +37 -0
  200. package/src/transport/dispatch/streaming/stream.ts +30 -2
  201. package/src/transport/dispatch/upstream.ts +9 -0
  202. package/src/transport/request/preparer.ts +7 -6
  203. package/src/transport/request/state.ts +9 -0
  204. package/src/transport/request/tenant-preferences.ts +60 -0
  205. package/src/transport/routing/route-catalog.ts +854 -799
  206. package/dist/dashboard/assets/BannedPage-MsQaeQTs.js.gz +0 -0
  207. package/dist/dashboard/assets/CliToolDetailPage-BO9nKg9w.js +0 -1
  208. package/dist/dashboard/assets/CliToolDetailPage-BO9nKg9w.js.gz +0 -0
  209. package/dist/dashboard/assets/CliToolsPage-DfWHTJ7v.js.gz +0 -0
  210. package/dist/dashboard/assets/CombosPage-BrCBQDAc.js.gz +0 -0
  211. package/dist/dashboard/assets/ConsoleLogPage-C6Uh33er.js.gz +0 -0
  212. package/dist/dashboard/assets/CustomizationPage-8aS9hyJu.js.gz +0 -0
  213. package/dist/dashboard/assets/HealthEventsModal-qomXThxX.js +0 -1
  214. package/dist/dashboard/assets/HealthEventsModal-qomXThxX.js.gz +0 -0
  215. package/dist/dashboard/assets/LoginPage-C8hKyXBe.js.gz +0 -0
  216. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
  217. package/dist/dashboard/assets/OverviewPage-C-ExCy1V.js +0 -1
  218. package/dist/dashboard/assets/OverviewPage-C-ExCy1V.js.gz +0 -0
  219. package/dist/dashboard/assets/ProviderDetailPage-tr811uaF.js +0 -3
  220. package/dist/dashboard/assets/ProviderDetailPage-tr811uaF.js.gz +0 -0
  221. package/dist/dashboard/assets/ProvidersPage-BS9IEpvv.js +0 -1
  222. package/dist/dashboard/assets/ProvidersPage-BS9IEpvv.js.gz +0 -0
  223. package/dist/dashboard/assets/ProxyPage-Rn_qXScb.js +0 -7
  224. package/dist/dashboard/assets/ProxyPage-Rn_qXScb.js.gz +0 -0
  225. package/dist/dashboard/assets/QuotaPage-BihkgQSq.js +0 -1
  226. package/dist/dashboard/assets/QuotaPage-BihkgQSq.js.gz +0 -0
  227. package/dist/dashboard/assets/SettingsPage-DhiEjm6T.js +0 -1
  228. package/dist/dashboard/assets/SettingsPage-DhiEjm6T.js.gz +0 -0
  229. package/dist/dashboard/assets/SetupPage-CXjUlfqr.js.gz +0 -0
  230. package/dist/dashboard/assets/SortableList-C-_oD3F2.js.gz +0 -0
  231. package/dist/dashboard/assets/StudioPage-u9IxmYOI.js.gz +0 -0
  232. package/dist/dashboard/assets/UsagePage-BEVLVrwP.js.gz +0 -0
  233. package/dist/dashboard/assets/api-keys-uNywYlfv.js.gz +0 -0
  234. package/dist/dashboard/assets/app-DxivpfYG.css +0 -2
  235. package/dist/dashboard/assets/app-DxivpfYG.css.gz +0 -0
  236. package/dist/dashboard/assets/assert-NHwYq95t.js.gz +0 -0
  237. package/dist/dashboard/assets/badge-6uzDDvqs.js.gz +0 -0
  238. package/dist/dashboard/assets/banner-4_WNg2WZ.js.gz +0 -0
  239. package/dist/dashboard/assets/card--AtFedAq.js.gz +0 -0
  240. package/dist/dashboard/assets/cli-tools-B3e7dx3N.js.gz +0 -0
  241. package/dist/dashboard/assets/clipboard-button-NreUx6c8.js.gz +0 -0
  242. package/dist/dashboard/assets/dialog-BtpsJU98.js.gz +0 -0
  243. package/dist/dashboard/assets/download-A0SW9OCW.js +0 -1
  244. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  245. package/dist/dashboard/assets/drawer-WwwtzihF.js.gz +0 -0
  246. package/dist/dashboard/assets/entry-Dj9AR0gY.js +0 -4
  247. package/dist/dashboard/assets/entry-Dj9AR0gY.js.gz +0 -0
  248. package/dist/dashboard/assets/entry-TJIye_jT.js +0 -1
  249. package/dist/dashboard/assets/entry-TJIye_jT.js.gz +0 -0
  250. package/dist/dashboard/assets/entry-jy9_4-Ke.js +0 -1
  251. package/dist/dashboard/assets/entry-jy9_4-Ke.js.gz +0 -0
  252. package/dist/dashboard/assets/github-badge-D_eM3IgG.js.gz +0 -0
  253. package/dist/dashboard/assets/index-DCBtROoK.js.gz +0 -0
  254. package/dist/dashboard/assets/inline-BcNGF8Gr.js.gz +0 -0
  255. package/dist/dashboard/assets/input-BG41et9G.js.gz +0 -0
  256. package/dist/dashboard/assets/layout-C8mer_Ak.js.gz +0 -0
  257. package/dist/dashboard/assets/list-row-Gh_W4OR_.js.gz +0 -0
  258. package/dist/dashboard/assets/live-BJGYRBnR.js.gz +0 -0
  259. package/dist/dashboard/assets/model-bans-DeVLDA7I.js +0 -1
  260. package/dist/dashboard/assets/model-bans-DeVLDA7I.js.gz +0 -0
  261. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  262. package/dist/dashboard/assets/numbers-Bs4LOOuf.js +0 -2
  263. package/dist/dashboard/assets/numbers-Bs4LOOuf.js.gz +0 -0
  264. package/dist/dashboard/assets/routing-CcusqXA7.js.gz +0 -0
  265. package/dist/dashboard/assets/select-DsX5QT8K.js.gz +0 -0
  266. package/dist/dashboard/assets/settings-mpmXTgUa.js.gz +0 -0
  267. package/dist/dashboard/assets/sparkles-BRaRUxAU.js +0 -1
  268. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  269. package/dist/dashboard/assets/stack-DwLmjUFK.js.gz +0 -0
  270. package/dist/dashboard/assets/state-DYdQmdAl.js.gz +0 -0
  271. package/dist/dashboard/assets/switch-CidSfbG7.js +0 -1
  272. package/dist/dashboard/assets/switch-CidSfbG7.js.gz +0 -0
  273. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +0 -1
  274. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  275. package/dist/dashboard/assets/useMutation-w4u4CVue.js.gz +0 -0
  276. /package/dist/dashboard/assets/{app-CafWtVXA.js → app-DCQZRmHY.js} +0 -0
  277. /package/dist/dashboard/assets/{app-CafWtVXA.js.gz → app-DCQZRmHY.js.gz} +0 -0
@@ -148,7 +148,62 @@ export const CONFIG_SPEC = {
148
148
  */
149
149
  CARTETHYIA_DISABLED_TASKS: { kind: "list" },
150
150
 
151
+ /**
152
+ * How long a day's telemetry is kept, in days.
153
+ *
154
+ * The retention unit is the whole day: a day's event file and its index are
155
+ * deleted together, and the current day is never touched. Past this age an
156
+ * individual request no longer exists anywhere, so its detail is gone —
157
+ * totals, breakdowns and spend are unaffected, because the index keeps
158
+ * running sums rather than the rows.
159
+ *
160
+ * This is the knob that decides how long the request list, per-request
161
+ * filters, and the detail drawer stay complete.
162
+ */
151
163
  CARTETHYIA_TELEMETRY_RETENTION_DAYS: { kind: "int", default: 30, min: 3, max: 365 },
164
+ /**
165
+ * Age at which a request's detail is described as no longer stored.
166
+ *
167
+ * Retention itself is `CARTETHYIA_TELEMETRY_RETENTION_DAYS`; this only words
168
+ * the message shown when a request that old is asked for. Kept because the
169
+ * message is operator-facing and was written against this knob.
170
+ */
171
+ CARTETHYIA_TELEMETRY_COMPACT_AFTER_DAYS: { kind: "int", default: 7, min: 1, max: 365 },
172
+ /**
173
+ * How often each day's index is brought up to date, in minutes.
174
+ *
175
+ * Bringing an index up to date means folding the bytes appended since the
176
+ * last pass — not re-reading the day. Reads fold before they answer too, so
177
+ * raising this does not make the numbers stale; it only leaves the on-disk
178
+ * index further behind, and the next read pays for the remainder.
179
+ */
180
+ CARTETHYIA_TELEMETRY_COMPACT_INTERVAL_MINUTES: {
181
+ kind: "int",
182
+ default: 60,
183
+ min: 5,
184
+ max: 1440,
185
+ },
186
+ /**
187
+ * Inert. Accepted so existing deployments keep booting; nothing reads it.
188
+ *
189
+ * It sized the one-file-per-window scheme — events were written to a file
190
+ * per this many minutes, 96 a day at the default. Events now append to one
191
+ * file per UTC day, so there is no window left to size.
192
+ */
193
+ CARTETHYIA_TELEMETRY_WINDOW_MINUTES: {
194
+ kind: "int",
195
+ default: 15,
196
+ min: 1,
197
+ max: 1440,
198
+ },
199
+ /**
200
+ * Inert. Accepted so existing deployments keep booting; nothing reads it.
201
+ *
202
+ * It capped how many window files one pass could fold. Folding is driven by
203
+ * a byte offset into the day file now and never deletes the file, so there
204
+ * is no per-pass batch to bound.
205
+ */
206
+ CARTETHYIA_TELEMETRY_COMPACT_MAX_FILES: { kind: "int", default: 48, min: 1, max: 10_000 },
152
207
  CARTETHYIA_TELEMETRY_PAYLOAD_MAX_BYTES: {
153
208
  kind: "int",
154
209
  default: 16 * 1024 * 1024,
@@ -157,7 +212,14 @@ export const CONFIG_SPEC = {
157
212
  },
158
213
 
159
214
  // Upstream request timeout + retry backoff
160
- CARTETHYIA_UPSTREAM_TIMEOUT_MS: { kind: "int", default: 120_000, min: 5_000, max: 600_000 },
215
+ //
216
+ // Strictly under 90s: a client whose own deadline is 90s must never be
217
+ // waiting on a gateway that is still holding a request open past it, and a
218
+ // provider that has not answered within 75s is not answering — the request
219
+ // is queued upstream or the egress is stuck, and the honest move is to fail
220
+ // over to another account rather than burn the caller's whole budget on one.
221
+ // 120s outlived the clients that call this, which cap lower.
222
+ CARTETHYIA_UPSTREAM_TIMEOUT_MS: { kind: "int", default: 75_000, min: 5_000, max: 600_000 },
161
223
  CARTETHYIA_STREAM_STALL_TIMEOUT_MS: {
162
224
  kind: "int",
163
225
  default: 360_000,
@@ -181,7 +243,24 @@ export const CONFIG_SPEC = {
181
243
  CARTETHYIA_ACCOUNT_QUOTA_COOLDOWN_MS: { kind: "int", default: 3_600_000, min: 0, max: 86_400_000 },
182
244
  CARTETHYIA_ACCOUNT_MODEL_CAPACITY_COOLDOWN_MS: { kind: "int", default: 120_000, min: 0, max: 86_400_000 },
183
245
  CARTETHYIA_ACCOUNT_TRANSIENT_COOLDOWN_MS: { kind: "int", default: 30_000, min: 0, max: 86_400_000 },
184
- CARTETHYIA_ACCOUNT_UNCLASSIFIED_COOLDOWN_MS: { kind: "int", default: 60_000, min: 0, max: 86_400_000 },
246
+ /**
247
+ * An unmapped upstream failure cools the account down for 15 minutes.
248
+ *
249
+ * It used to be 60s, which put the account straight back into rotation and
250
+ * re-dialled an upstream that had just answered something this gateway does
251
+ * not understand — so the same unknown error was retried every minute, and
252
+ * an account that was actually broken contributed a steady stream of failed
253
+ * attempts instead of one. Fifteen minutes is long enough for a transient
254
+ * provider condition to clear on its own and short enough that no operator
255
+ * action is needed to recover it. It is still a cooldown with a deadline,
256
+ * never `disabled`, so nothing here can strand an account.
257
+ */
258
+ CARTETHYIA_ACCOUNT_UNCLASSIFIED_COOLDOWN_MS: {
259
+ kind: "int",
260
+ default: 900_000,
261
+ min: 0,
262
+ max: 86_400_000,
263
+ },
185
264
  CARTETHYIA_ACCOUNT_COOLDOWN_ESCALATION_MAX_MS: { kind: "int", default: 3_600_000, min: 0, max: 86_400_000 },
186
265
  CARTETHYIA_POOL_COOLDOWN_MS: { kind: "int", default: 120_000, min: 0, max: 86_400_000 },
187
266
  CARTETHYIA_ACCOUNT_AUTH_REJECTED_COOLDOWN_MS: { kind: "int", default: 300_000, min: 0, max: 86_400_000 },
@@ -19,3 +19,49 @@ export function resolveTelemetryPayloadMaxBytes(): number {
19
19
  CONFIG_SPEC.CARTETHYIA_TELEMETRY_PAYLOAD_MAX_BYTES,
20
20
  );
21
21
  }
22
+
23
+ /**
24
+ * Age at which hourly telemetry files are compacted into a per-day summary.
25
+ *
26
+ * Clamped below the retention window: compacting at or past the delete age
27
+ * would fold a file and throw it away in the same pass, spending the read for
28
+ * totals that were about to be discarded.
29
+ */
30
+ export function resolveTelemetryCompactAfterDays(): number {
31
+ const compacted = readInt(
32
+ "CARTETHYIA_TELEMETRY_COMPACT_AFTER_DAYS",
33
+ CONFIG_SPEC.CARTETHYIA_TELEMETRY_COMPACT_AFTER_DAYS,
34
+ );
35
+ return Math.min(compacted, resolveTelemetryRetentionDays());
36
+ }
37
+
38
+ /** Minutes between compaction passes. */
39
+ export function resolveTelemetryCompactIntervalMs(): number {
40
+ const minutes = readInt(
41
+ "CARTETHYIA_TELEMETRY_COMPACT_INTERVAL_MINUTES",
42
+ CONFIG_SPEC.CARTETHYIA_TELEMETRY_COMPACT_INTERVAL_MINUTES,
43
+ );
44
+ return minutes * 60_000;
45
+ }
46
+
47
+ /**
48
+ * Minutes of events per source file, before it is folded into the day summary.
49
+ *
50
+ * Defaults to the sweep interval so exactly one window closes per pass: a
51
+ * window longer than the interval would leave files waiting several passes, and
52
+ * a shorter one would let several close between passes.
53
+ */
54
+ export function resolveTelemetryWindowMinutes(): number {
55
+ return readInt(
56
+ "CARTETHYIA_TELEMETRY_WINDOW_MINUTES",
57
+ CONFIG_SPEC.CARTETHYIA_TELEMETRY_WINDOW_MINUTES,
58
+ );
59
+ }
60
+
61
+ /** Maximum hourly files folded in one compaction pass. */
62
+ export function resolveTelemetryCompactMaxFiles(): number {
63
+ return readInt(
64
+ "CARTETHYIA_TELEMETRY_COMPACT_MAX_FILES",
65
+ CONFIG_SPEC.CARTETHYIA_TELEMETRY_COMPACT_MAX_FILES,
66
+ );
67
+ }
@@ -2,7 +2,7 @@ import { eq } from "drizzle-orm";
2
2
 
3
3
  import type { CartethyiaDatabase } from "../../persistence/db-handle";
4
4
  import { apiKeys } from "../../persistence/schema";
5
- import { queryTelemetryEvents } from "../../observability/event-log";
5
+ import { queryIndexedBuckets } from "../../observability/event-day-index";
6
6
  import { utcDayStart, utcMonthStart } from "../shared/day";
7
7
 
8
8
  /**
@@ -33,9 +33,20 @@ export async function familyBucketSpend(
33
33
  .where(eq(apiKeys.parentKeyId, keyId));
34
34
  const keyIds = [keyId, ...children.map((child) => child.id)];
35
35
  const spentSince = async (start: Date): Promise<number> => {
36
- const events = await queryTelemetryEvents({ apiKeyIds: keyIds, since: start });
37
- return events
38
- .reduce((sum, event) => sum + (event.inputTokens ?? 0) + (event.outputTokens ?? 0), 0);
36
+ // Folded first, so the index covers every event written so far including the
37
+ // current day's — then read from the index alone.
38
+ //
39
+ // Deliberately not index *plus* events: they hold the same requests, and
40
+ // adding them doubled every bucket. Folding first is what makes the index
41
+ // sufficient on its own; the watermark is the point up to which it is
42
+ // complete.
43
+ const allowed = new Set(keyIds);
44
+ const indexed = await queryIndexedBuckets({ since: start, foldFirst: true }).catch(
45
+ () => [],
46
+ );
47
+ return indexed
48
+ .filter((bucket) => bucket.apiKeyId !== null && allowed.has(bucket.apiKeyId))
49
+ .reduce((sum, bucket) => sum + bucket.inputTokens + bucket.outputTokens, 0);
39
50
  };
40
51
  const [daily, monthly] = await Promise.all([
41
52
  spentSince(utcDayStart(now)),
@@ -20,6 +20,7 @@
20
20
  * as sensitive as the database.
21
21
  */
22
22
  import { getTableColumns, getTableName, type Column, type Table } from "drizzle-orm";
23
+ import { resolveMaxBodyBytes } from "../../config/runtime";
23
24
  import {
24
25
  apiKeys,
25
26
  cliToolMappings,
@@ -45,8 +46,20 @@ import {
45
46
  export const BACKUP_APP = "cartethyia";
46
47
  export const BACKUP_VERSION = 1;
47
48
 
48
- /** Upper bound for restore payloads; the listener enforces the same ceiling. */
49
- export const MAX_BACKUP_BYTES = 8 * 1024 * 1024 * 1024;
49
+ /**
50
+ * Upper bound for restore payloads.
51
+ *
52
+ * Deliberately not larger than the listener's own body cap. It used to claim
53
+ * 8 GiB while the server admitted 4 MiB, so the number described a limit that
54
+ * could never be reached: a file above the listener's cap is cut off before
55
+ * this check runs, and a large upload fails as a reset connection rather than
56
+ * the 413 this constant promises. The bound now states what the install
57
+ * actually admits, so the message an operator sees matches what happened.
58
+ *
59
+ * Raise `CARTETHYIA_SERVER_MAX_BODY_BYTES` to accept larger backups; this
60
+ * constant follows it rather than the two drifting apart again.
61
+ */
62
+ export const MAX_BACKUP_BYTES = resolveMaxBodyBytes();
50
63
 
51
64
  /** Defense-in-depth row cap per table, after the byte bound. */
52
65
  export const MAX_ROWS_PER_TABLE = 500_000;
@@ -78,6 +78,24 @@ const BACKUP_SCOPES = ["dashboard:write"] as const;
78
78
  const importBody = t.Object({
79
79
  password: t.String(),
80
80
  backup: t.Unknown(),
81
+ /**
82
+ * Table names to restore. Absent or empty restores every table in the file,
83
+ * which is what the dashboard's checklist defaults to and what the API did
84
+ * before selection existed.
85
+ */
86
+ tables: t.Optional(t.Array(t.String())),
87
+ });
88
+
89
+ /**
90
+ * The inspect body: a file, no password.
91
+ *
92
+ * Inspection writes nothing and reads no secret — it only reports which tables
93
+ * the file carries and which of them the live schema has — so it needs no
94
+ * re-authentication. Requiring the console password here would mean typing it
95
+ * twice for one restore.
96
+ */
97
+ const inspectBody = t.Object({
98
+ backup: t.Unknown(),
81
99
  });
82
100
 
83
101
  const exportQuery = t.Object({
@@ -164,7 +182,12 @@ export function createBackupRoutes(config: BackupRoutesConfig): Elysia {
164
182
  // from nowhere else. A request body may not name the tenant it writes
165
183
  // into: that would let any holder of the scope import into, and
166
184
  // overwrite, a tenant they are not.
167
- const result = await config.backupFor(request).restore(body.password, body.backup, access.tenantId);
185
+ const result = await config.backupFor(request).restore(
186
+ body.password,
187
+ body.backup,
188
+ access.tenantId,
189
+ body.tables === undefined ? undefined : { tables: body.tables },
190
+ );
168
191
  // Routing-visible write: the restore has committed, so the cached
169
192
  // snapshot must be rebuilt before the next `/v1/*` dispatch.
170
193
  await config.snapshotInvalidator?.invalidate();
@@ -214,5 +237,33 @@ export function createBackupRoutes(config: BackupRoutesConfig): Elysia {
214
237
  } catch (error) {
215
238
  return errorResponse(error, set, "Delete all failed");
216
239
  }
240
+ })
241
+ .post("/inspect", { body: inspectBody }, async ({ request, body, set }) => {
242
+ try {
243
+ const access = requireAnyScope(config.accessResolver(request), BACKUP_SCOPES);
244
+ if (access.tenantId === null) {
245
+ throw new ConsoleDomainError("tenant_required", 403, "Tenant isolation required");
246
+ }
247
+ // Same byte bound as the import, for the same reason: the file is
248
+ // parsed here too, and an unbounded one would be buffered before any
249
+ // validation could refuse it.
250
+ const declared = Number(request.headers.get("content-length") ?? "0");
251
+ const actualBytes = JSON.stringify(body ?? null).length;
252
+ if (
253
+ (Number.isFinite(declared) && declared > MAX_BACKUP_BYTES) ||
254
+ actualBytes > MAX_BACKUP_BYTES
255
+ ) {
256
+ set.status = 413;
257
+ return {
258
+ error: `backup exceeds the ${MAX_BACKUP_BYTES} byte limit`,
259
+ code: "request_too_large",
260
+ };
261
+ }
262
+ // Reads nothing and writes nothing: the checklist the restore wizard
263
+ // shows, so an operator can pick sections before typing a password.
264
+ return config.backupFor(request).inspect(body.backup);
265
+ } catch (error) {
266
+ return errorResponse(error, set, "Backup inspection failed");
267
+ }
217
268
  }) as unknown as Elysia;
218
269
  }
@@ -19,6 +19,7 @@
19
19
  * re-authentication.
20
20
  */
21
21
  import type { CartethyiaDatabase } from "../../persistence/db-handle";
22
+ import type { Table } from "drizzle-orm";
22
23
  import {
23
24
  models,
24
25
  providerAccounts,
@@ -29,7 +30,12 @@ import {
29
30
  } from "../../persistence/schema";
30
31
  import { ConsoleDomainError } from "../shared/errors";
31
32
  import { tablesForSection, tableName } from "./contracts";
32
- import type { BackupSection, RestoreWarning, ValidatedRestore } from "./contracts";
33
+ import type {
34
+ BackupSection,
35
+ RestoreWarning,
36
+ ValidatedRestore,
37
+ ValidatedTable,
38
+ } from "./contracts";
33
39
  import {
34
40
  applyRestore,
35
41
  decompressBackup,
@@ -90,6 +96,53 @@ export interface ImportResult {
90
96
  readonly format: "native" | "nine_router";
91
97
  }
92
98
 
99
+ /**
100
+ * Which parts of a backup file a restore should actually write.
101
+ *
102
+ * Absent or empty means "everything", which is the historical behavior and
103
+ * what the dashboard's checklist defaults to. A table named here but absent
104
+ * from the file is simply not restored — it is not an error, because the
105
+ * checklist is built from an inspection of *this* file and a selection never
106
+ * invents a table.
107
+ */
108
+ export interface BackupRestoreSelection {
109
+ /** Table names to restore; absent or empty restores every table present. */
110
+ readonly tables?: readonly string[];
111
+ }
112
+
113
+ /**
114
+ * Narrows a validated backup to the tables the operator selected.
115
+ *
116
+ * The tenant row is the one thing that is not independently selectable: it is
117
+ * not a table in either section (`BACKUP_SECTIONS` are config and telemetry),
118
+ * so it never appears on the checklist, and every tenant-scoped table
119
+ * cascades from it. Dropping it because it was not named would make a partial
120
+ * restore fail on a foreign key for a table the operator *did* ask for. So it
121
+ * rides along with any config selection and is written only when one is made.
122
+ *
123
+ * A table named in the selection but missing from the file is a no-op rather
124
+ * than an error: the checklist is built from an inspection of this file, but
125
+ * the selection is validated as names only, and refusing to restore anything
126
+ * over one stale name would be worse than ignoring it.
127
+ */
128
+ function restrictToSelection(
129
+ validation: ValidatedRestore,
130
+ selection: BackupRestoreSelection | undefined,
131
+ ): ValidatedRestore {
132
+ const wanted = selection?.tables;
133
+ if (wanted === undefined || wanted.length === 0) return validation;
134
+ const selected = new Set(wanted);
135
+ const tables = new Map<Table, ValidatedTable>();
136
+ for (const [table, entry] of validation.tables) {
137
+ if (selected.has(tableName(table))) tables.set(table, entry);
138
+ }
139
+ return {
140
+ ...validation,
141
+ tables,
142
+ tenantRows: tables.size > 0 ? validation.tenantRows : [],
143
+ };
144
+ }
145
+
93
146
  function sectionsOrDefault(sections: readonly BackupSection[] | undefined): readonly BackupSection[] {
94
147
  if (sections === undefined || sections.length === 0) return ["config"];
95
148
  for (const section of sections) {
@@ -246,6 +299,7 @@ export class BackupService {
246
299
  password: unknown,
247
300
  payload: unknown,
248
301
  tenantId: string,
302
+ selection?: BackupRestoreSelection,
249
303
  ): Promise<ImportResult> {
250
304
  if (typeof password !== "string" || !(await this.options.verifyPassword(password))) {
251
305
  throw new ConsoleDomainError("unauthorized", 401, "password is incorrect");
@@ -258,11 +312,11 @@ export class BackupService {
258
312
 
259
313
  if (detected.kind === "nine_router") {
260
314
  const { payload: converted, report } = convert9RouterBackup(detected.payload, tenantId);
261
- const result = await this.apply(converted, tenantId);
315
+ const result = await this.apply(converted, tenantId, selection);
262
316
  return { ...result, report, format: "nine_router" };
263
317
  }
264
318
 
265
- const result = await this.apply(detected.payload, tenantId);
319
+ const result = await this.apply(detected.payload, tenantId, selection);
266
320
  return { ...result, format: "native" };
267
321
  }
268
322
 
@@ -348,13 +402,15 @@ export class BackupService {
348
402
  private async apply(
349
403
  payload: unknown,
350
404
  tenantId: string,
405
+ selection?: BackupRestoreSelection,
351
406
  ): Promise<{ restored: Record<string, number>; skipped: Record<string, number>; warnings: readonly RestoreWarning[] }> {
352
407
  const validation = validateRestorePayload(payload, tenantId);
353
408
  if (!validation.ok) {
354
409
  throw new ConsoleDomainError("invalid_request", 400, validation.error);
355
410
  }
356
411
  try {
357
- const compatible = await omitRowsForUnavailableProviders(this.options.db, validation.value, tenantId);
412
+ const scoped = restrictToSelection(validation.value, selection);
413
+ const compatible = await omitRowsForUnavailableProviders(this.options.db, scoped, tenantId);
358
414
  const applied = await applyRestore(this.options.db, compatible, restoreOrder(), tenantId);
359
415
  return { ...applied, warnings: compatible.warnings };
360
416
  } catch (error) {
@@ -48,6 +48,13 @@ export interface ConsoleApiCompositionDeps {
48
48
  readonly routeSnapshotService: { invalidate(): Promise<number> };
49
49
  readonly poolSelector: NetworkPoolSelector;
50
50
  readonly telemetryBuffer: TelemetryBatchBuffer;
51
+ /**
52
+ * Queued writers the console can drain on demand (telemetry, usage, health
53
+ * events). Optional for the same reason the other reduced composition
54
+ * surface is: absent means the flush control is not offered rather than the
55
+ * console failing to build.
56
+ */
57
+ readonly flushBuffers?: { flush(): Promise<void> };
51
58
  /**
52
59
  * The request state store owns the live in-flight gauge, so the console reads
53
60
  * its snapshot/subscription from here rather than a free-standing module.
@@ -189,6 +196,7 @@ export function createConsoleRouter(deps: ConsoleApiCompositionDeps): Elysia {
189
196
  admissionService: deps.admissionService,
190
197
  ...(deps.modelStrikes ? { modelStrikes: deps.modelStrikes } : {}),
191
198
  readRoutingAccountInflight: deps.readRoutingAccountInflight,
199
+ ...(deps.flushBuffers ? { flushBuffers: deps.flushBuffers } : {}),
192
200
  credentialService,
193
201
  // The backup surface re-authenticates the operator, so it needs the current
194
202
  // user's hash. Read from the session on the request that asks for it, never
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Disk housekeeping: what the telemetry and payload directories hold, and the
3
+ * reclaim actions an operator can run from the console.
4
+ *
5
+ * Everything here is files on disk, except one deliberate row-level action.
6
+ * Reclaiming a file is safe because the data it held is already past the point
7
+ * where the gateway would read it: payload frames expire 15 minutes after they
8
+ * are written, and telemetry files past the retention window are outside every
9
+ * query's scan bound. Deleting rows is a different decision with a different
10
+ * blast radius, and the only row data the console offers purging is the health
11
+ * & error log — purely an operator's diagnostic trail with no request-path
12
+ * reader, unlike everything else in this panel.
13
+ *
14
+ * Sizes are bytes as `number`. A directory is scanned on each read rather than
15
+ * cached, because the point of the panel is to show what is true now — a
16
+ * stale size would be worse than none.
17
+ */
18
+
19
+ /** One measured category of on-disk data. */
20
+ export interface DiskUsageBucket {
21
+ /** Stable id, so the dashboard can key rows without matching on copy. */
22
+ readonly id: "telemetry_hourly" | "telemetry_compacted" | "payload_frames";
23
+ readonly label: string;
24
+ readonly bytes: number;
25
+ readonly files: number;
26
+ /** Why this data exists, shown next to the size. */
27
+ readonly note: string;
28
+ /** Bytes reclaimable right now, when that is knowable without a dry run. */
29
+ readonly reclaimableBytes: number | null;
30
+ readonly reclaimableFiles: number | null;
31
+ }
32
+
33
+ export interface DiskUsageResponse {
34
+ readonly buckets: readonly DiskUsageBucket[];
35
+ readonly totalBytes: number;
36
+ /** Retention in days; telemetry older than this is outside every scan. */
37
+ readonly retentionDays: number;
38
+ /** Payload frames live this long before they are reclaimable. */
39
+ readonly payloadRetentionMs: number;
40
+ /**
41
+ * Stored health & error rows, for the purge row's label. Absent when the
42
+ * composition does not wire the health-event store (a reduced console),
43
+ * which is also when the purge control is not offered.
44
+ */
45
+ readonly healthEventCount?: number;
46
+ }
47
+
48
+ export type ReclaimAction = "payload_frames" | "telemetry_retention" | "telemetry_hourly";
49
+
50
+ export interface ReclaimResponse {
51
+ readonly action: ReclaimAction;
52
+ readonly deletedFiles: number;
53
+ readonly reclaimedBytes: number;
54
+ }
55
+
56
+ /** Row-level deletion targets. The health & error log is the only one. */
57
+ export type PurgeTarget = "health_events";
58
+
59
+ export interface PurgeResponse {
60
+ readonly target: PurgeTarget;
61
+ /** Rows actually deleted — the number the operator's decision produced. */
62
+ readonly deleted: number;
63
+ }
64
+
65
+ export interface FlushResponse {
66
+ readonly flushed: boolean;
67
+ }
68
+
69
+ /**
70
+ * A reclaim the caller is not allowed to make.
71
+ *
72
+ * Kept as a named class so the route maps it to 403 rather than letting it
73
+ * surface as a 500 — an operator asking for something out of policy is a
74
+ * normal request, not a failure.
75
+ */
76
+ export class ReclaimNotAllowedError extends Error {
77
+ readonly status = 403;
78
+
79
+ constructor(message: string) {
80
+ super(message);
81
+ this.name = "ReclaimNotAllowedError";
82
+ }
83
+ }
84
+
85
+ /** A purge whose backing store is not wired in this composition. */
86
+ export class PurgeNotWiredError extends Error {
87
+ readonly status = 501;
88
+
89
+ constructor() {
90
+ super("Health & error log purging is not offered in this deployment's console composition.");
91
+ this.name = "PurgeNotWiredError";
92
+ }
93
+ }