cartethyia 2.5.4 → 2.5.7

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 (395) hide show
  1. package/.env.example +13 -4
  2. package/README.md +2 -2
  3. package/dist/dashboard/assets/{BannedPage-C2orBsI2.js → BannedPage-DE2607Tb.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-DE2607Tb.js.gz +0 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-Duv6mfrl.js +1 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-Duv6mfrl.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-BV6pxVQo.js → CliToolsPage-C2PNYWzk.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-C2PNYWzk.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-TXP9OfAM.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-TXP9OfAM.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-CW-wPmJ6.js → ConsoleLogPage-B-9xnAQN.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-B-9xnAQN.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-0p8uX7vB.js → CustomizationPage-xeax9nJt.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-xeax9nJt.js.gz +0 -0
  15. package/dist/dashboard/assets/{HealthEventsModal-BjqSMfko.js → HealthEventsModal-Df4jjaHJ.js} +1 -1
  16. package/dist/dashboard/assets/HealthEventsModal-Df4jjaHJ.js.gz +0 -0
  17. package/dist/dashboard/assets/{LoginPage-BvzCx8Kr.js → LoginPage-Dyojsm84.js} +1 -1
  18. package/dist/dashboard/assets/LoginPage-Dyojsm84.js.gz +0 -0
  19. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js +1 -0
  20. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-BJfg4ow6.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-BJfg4ow6.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-ChCX1gmT.js +3 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-ChCX1gmT.js.gz +0 -0
  25. package/dist/dashboard/assets/{ProvidersPage-Js9jUPSb.js → ProvidersPage-DeWSOEUO.js} +1 -1
  26. package/dist/dashboard/assets/ProvidersPage-DeWSOEUO.js.gz +0 -0
  27. package/dist/dashboard/assets/ProxyPage-CkpWln2S.js +7 -0
  28. package/dist/dashboard/assets/ProxyPage-CkpWln2S.js.gz +0 -0
  29. package/dist/dashboard/assets/QuotaPage-B3VOgQmt.js +1 -0
  30. package/dist/dashboard/assets/QuotaPage-B3VOgQmt.js.gz +0 -0
  31. package/dist/dashboard/assets/SettingsPage-C_klIZom.js +1 -0
  32. package/dist/dashboard/assets/SettingsPage-C_klIZom.js.gz +0 -0
  33. package/dist/dashboard/assets/{SetupPage-DDbMQsoo.js → SetupPage-BYph-InK.js} +1 -1
  34. package/dist/dashboard/assets/SetupPage-BYph-InK.js.gz +0 -0
  35. package/dist/dashboard/assets/{SortableList-CB3Z7VTf.js → SortableList-CvyQCYWJ.js} +1 -1
  36. package/dist/dashboard/assets/SortableList-CvyQCYWJ.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-D95UCPHI.js → StudioPage-Cm1JYilI.js} +3 -3
  38. package/dist/dashboard/assets/StudioPage-Cm1JYilI.js.gz +0 -0
  39. package/dist/dashboard/assets/UsagePage-DlFnc3_S.js +56 -0
  40. package/dist/dashboard/assets/UsagePage-DlFnc3_S.js.gz +0 -0
  41. package/dist/dashboard/assets/api-keys-DQYLQxpW.js +1 -0
  42. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  43. package/dist/dashboard/assets/app-CX_Z8cMs.css +2 -0
  44. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  45. package/dist/dashboard/assets/{assert-Chi7pCnt.js → assert-C9FD_JwJ.js} +1 -1
  46. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  47. package/dist/dashboard/assets/{badge-D4pQapKj.js → badge-CR72dasT.js} +1 -1
  48. package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
  49. package/dist/dashboard/assets/{banner-DH6cg-_B.js → banner-CI9_eSfO.js} +1 -1
  50. package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
  51. package/dist/dashboard/assets/{card-BKqlUOe4.js → card-DS7CQEsO.js} +1 -1
  52. package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
  53. package/dist/dashboard/assets/{cli-tools-fTgljTGA.js → cli-tools-CNqzrfdb.js} +1 -1
  54. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  55. package/dist/dashboard/assets/{clipboard-button-D5Ys_sM0.js → clipboard-button-611pSsaX.js} +1 -1
  56. package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
  57. package/dist/dashboard/assets/{fingerprint-pattern-BWuHtbW4.js → concurrency-BDF0ok7y.js} +1 -1
  58. package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
  59. package/dist/dashboard/assets/dialog-k37exjNt.js +1 -0
  60. package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
  61. package/dist/dashboard/assets/drawer-CX7N8ucR.js +1 -0
  62. package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
  63. package/dist/dashboard/assets/{entry-CQBr6U9c.js → entry-B9Xvtnfj.js} +1 -1
  64. package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-Bog0p925.js +4 -0
  66. package/dist/dashboard/assets/entry-Bog0p925.js.gz +0 -0
  67. package/dist/dashboard/assets/entry-CTP8AyiS.js +1 -0
  68. package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
  69. package/dist/dashboard/assets/{github-badge-BlptL8Uf.js → github-badge-sltk1qUi.js} +1 -1
  70. package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
  71. package/dist/dashboard/assets/{index-js6TJvdr.js → index-CZQIrdoX.js} +2 -2
  72. package/dist/dashboard/assets/index-CZQIrdoX.js.gz +0 -0
  73. package/dist/dashboard/assets/{inline-DUWDSmDV.js → inline-BhY2sSnO.js} +1 -1
  74. package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
  75. package/dist/dashboard/assets/{input-LVjgZq4f.js → input-V1umFiBk.js} +1 -1
  76. package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
  77. package/dist/dashboard/assets/layout-BBOYycyS.js +1 -0
  78. package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
  79. package/dist/dashboard/assets/{list-row-DupvLfDx.js → list-row-1zzZYM0B.js} +1 -1
  80. package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
  81. package/dist/dashboard/assets/{live-C4pEJUqI.js → live-BivGfuUs.js} +1 -1
  82. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  83. package/dist/dashboard/assets/{model-bans-BRiuufkB.js → model-bans-CmffjLuG.js} +1 -1
  84. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  85. package/dist/dashboard/assets/{mutation-B4ROwk8L.js → mutation-46P0oWMe.js} +1 -1
  86. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  87. package/dist/dashboard/assets/numbers-DkfMR5Qs.js +2 -0
  88. package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
  89. package/dist/dashboard/assets/{query-keys-gmjzC3RS.js → query-keys-BFhGpwR1.js} +1 -1
  90. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  91. package/dist/dashboard/assets/{routing-C7tBT5o-.js → routing-CAb4F37R.js} +1 -1
  92. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  93. package/dist/dashboard/assets/{select-WUCXorXw.js → select-BPpSsBgL.js} +1 -1
  94. package/dist/dashboard/assets/select-BPpSsBgL.js.gz +0 -0
  95. package/dist/dashboard/assets/{settings-C_zKl1GS.js → settings-DNX9sydi.js} +1 -1
  96. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  97. package/dist/dashboard/assets/{stack-_3O0F8Xn.js → stack-BHTfXdz_.js} +1 -1
  98. package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
  99. package/dist/dashboard/assets/{state-BgTxHcJS.js → state-mPDRlQkl.js} +1 -1
  100. package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
  101. package/dist/dashboard/assets/{switch-DEjTtOWf.js → switch-DOmJxg7H.js} +1 -1
  102. package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
  103. package/dist/dashboard/assets/tokens-COzario9.js +1 -0
  104. package/dist/dashboard/assets/tokens-COzario9.js.gz +0 -0
  105. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js +1 -0
  106. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js.gz +0 -0
  107. package/dist/dashboard/assets/{useMutation-BWSC0UAS.js → useMutation-Dem0lXQj.js} +1 -1
  108. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  109. package/dist/dashboard/index.html +1 -1
  110. package/dist/dashboard/index.html.gz +0 -0
  111. package/dist/dashboard/precompressed-assets.json +54 -54
  112. package/migrations/README.md +52 -52
  113. package/migrations/lite-sqlite3/0000_baseline.sql +2 -2
  114. package/package.json +95 -95
  115. package/scripts/cli/admin.ts +125 -0
  116. package/scripts/cli/{ui/background.ts → background.ts} +1 -1
  117. package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -244
  118. package/scripts/cli/doctor.ts +37 -0
  119. package/scripts/cli/{support/env.ts → env.ts} +99 -99
  120. package/scripts/cli/{gateway/spawn.ts → gateway.ts} +112 -24
  121. package/scripts/cli/instance.ts +224 -0
  122. package/scripts/cli/main.ts +61 -18
  123. package/scripts/cli/{support/platform.ts → platform.ts} +64 -64
  124. package/scripts/cli/port.ts +100 -0
  125. package/scripts/cli/{support/services.ts → services.ts} +88 -88
  126. package/scripts/cli/{command/setup.ts → setup.ts} +35 -34
  127. package/scripts/cli/{command/start.ts → start.ts} +10 -5
  128. package/scripts/cli/{support/startup-signal.ts → startup-signal.ts} +23 -23
  129. package/scripts/cli/tray/index.ts +62 -12
  130. package/scripts/cli/{command/tray.ts → tray.ts} +208 -208
  131. package/scripts/cli/{command/update.ts → update.ts} +39 -39
  132. package/scripts/cli/version.ts +67 -0
  133. package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
  134. package/src/boot/app.ts +697 -693
  135. package/src/boot/dependencies.ts +647 -646
  136. package/src/boot/timeout.ts +2 -13
  137. package/src/config/cooldowns.ts +186 -138
  138. package/src/config/network.ts +8 -14
  139. package/src/config/providers.ts +7 -10
  140. package/src/config/runtime.ts +76 -76
  141. package/src/config/security.ts +15 -0
  142. package/src/config/spec.ts +20 -30
  143. package/src/config/telemetry.ts +0 -20
  144. package/src/config/timeouts.ts +0 -6
  145. package/src/console/admin/model-abuse/contracts.ts +28 -6
  146. package/src/console/admin/model-abuse/routes.ts +56 -3
  147. package/src/console/api-keys/contracts.ts +410 -414
  148. package/src/console/api-keys/routes.ts +881 -880
  149. package/src/console/auth/credentials.ts +3 -3
  150. package/src/console/auth/setup-service.ts +2 -2
  151. package/src/console/backup/export-endpoint.ts +6 -2
  152. package/src/console/backup/nine-router.ts +2 -5
  153. package/src/console/backup/service.ts +4 -0
  154. package/src/console/backup/store.ts +192 -29
  155. package/src/console/backup/validate.ts +3 -45
  156. package/src/console/cli-tools/fs-ops.ts +487 -487
  157. package/src/console/console-router-guards.ts +1 -1
  158. package/src/console/data-management/contracts.ts +13 -6
  159. package/src/console/data-management/operations.ts +23 -18
  160. package/src/console/data-management/routes.ts +0 -1
  161. package/src/console/domain-registration.ts +10 -5
  162. package/src/console/observability/live.ts +22 -0
  163. package/src/console/observability/store.ts +39 -16
  164. package/src/console/observability/usage-dimensions.ts +4 -4
  165. package/src/console/providers/catalog/catalog-projections.ts +6 -7
  166. package/src/console/providers/catalog/model-operations.ts +5 -2
  167. package/src/console/providers/catalog/provider-operations.ts +4 -4
  168. package/src/console/providers/catalog/public-model-store.ts +8 -11
  169. package/src/console/providers/catalog/store.ts +1553 -1553
  170. package/src/console/providers/oauth/routes.ts +8 -12
  171. package/src/console/quota/account-quota-tenant.ts +6 -6
  172. package/src/console/quota/quota-view.ts +1 -1
  173. package/src/console/remote-support/contracts.ts +61 -0
  174. package/src/console/remote-support/routes.ts +65 -0
  175. package/src/console/remote-support/service.ts +207 -0
  176. package/src/console/response-compression.ts +0 -3
  177. package/src/console/routing/pools/probe-result.ts +3 -2
  178. package/src/console/routing/pools/probe-verdict.ts +132 -132
  179. package/src/console/routing/pools/store.ts +8 -8
  180. package/src/console/settings/contracts.ts +58 -2
  181. package/src/console/settings/store.ts +30 -3
  182. package/src/console/shared/cursor.ts +8 -8
  183. package/src/console/shared/errors.ts +1 -3
  184. package/src/network/http2-bidi.ts +2 -4
  185. package/src/network/pool/agent.ts +478 -478
  186. package/src/network/pool/selector.ts +5 -4
  187. package/src/network/retry.ts +1 -2
  188. package/src/network/ssrf.ts +5 -4
  189. package/src/observability/event-compaction.ts +80 -78
  190. package/src/observability/event-day-index.ts +13 -15
  191. package/src/observability/event-log.ts +4 -4
  192. package/src/observability/logger.ts +6 -6
  193. package/src/observability/payload-capture.ts +151 -33
  194. package/src/observability/payload-store.ts +397 -440
  195. package/src/observability/redaction.ts +5 -6
  196. package/src/observability/request-notify.ts +43 -0
  197. package/src/observability/telemetry-status.ts +8 -9
  198. package/src/observability/token-speed.ts +5 -4
  199. package/src/persistence/db-mode.ts +7 -7
  200. package/src/persistence/readiness.ts +1 -1
  201. package/src/persistence/schema.ts +1406 -1402
  202. package/src/persistence/shared-cache.ts +1 -2
  203. package/src/persistence/stores/api-key-store.ts +18 -6
  204. package/src/persistence/stores/share-store.ts +4 -3
  205. package/src/persistence/stores/telemetry-store.ts +236 -237
  206. package/src/protocol/canonical-model.ts +11 -3
  207. package/src/protocol/codecs.ts +138 -4
  208. package/src/protocol/gateway-error.ts +6 -4
  209. package/src/protocol/request/chat.ts +21 -1
  210. package/src/protocol/request/codex.ts +10 -1
  211. package/src/protocol/request/messages.ts +4 -5
  212. package/src/protocol/request/responses.ts +9 -1
  213. package/src/protocol/response/chat.ts +34 -4
  214. package/src/protocol/response/codex.ts +7 -4
  215. package/src/protocol/response/dsml.ts +140 -28
  216. package/src/protocol/response/responses.ts +11 -5
  217. package/src/protocol/stream-error-frames.ts +4 -5
  218. package/src/providers/authentication/jwt-validator.ts +9 -10
  219. package/src/providers/authentication/oauth-refresh-service.ts +9 -11
  220. package/src/providers/configured-openai-providers.ts +134 -136
  221. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  222. package/src/providers/discovery/catalog/catalog-format.ts +3 -6
  223. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  224. package/src/providers/discovery/probe-phases.ts +4 -4
  225. package/src/providers/discovery/probe-wire.ts +6 -13
  226. package/src/providers/discovery/probing-service.ts +1117 -1045
  227. package/src/providers/integrations/alibabacp/alibabacp.ts +1 -1
  228. package/src/providers/integrations/antigravity/antigravity-quota.ts +5 -7
  229. package/src/providers/integrations/buddy/buddy-chat-shared.ts +23 -2
  230. package/src/providers/integrations/buddy/buddy-checkin.ts +1 -4
  231. package/src/providers/integrations/buddy/buddy-cli-headers.ts +23 -6
  232. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -1
  233. package/src/providers/integrations/buddy/buddy-identity.ts +52 -0
  234. package/src/providers/integrations/buddy/buddy-media.ts +2 -2
  235. package/src/providers/integrations/buddy/buddy-usage-store.ts +3 -4
  236. package/src/providers/integrations/buddy/codebuddy/quota.ts +2 -13
  237. package/src/providers/integrations/buddy/codebuddy/shared.ts +118 -115
  238. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -11
  239. package/src/providers/integrations/buddy/workbuddy/shared.ts +5 -2
  240. package/src/providers/integrations/claude/claude.ts +608 -608
  241. package/src/providers/integrations/cline/cline-quota.ts +132 -132
  242. package/src/providers/integrations/cline/cline.ts +537 -539
  243. package/src/providers/integrations/codex/codex-oauth.ts +1 -1
  244. package/src/providers/integrations/commandcode/commandcode.ts +674 -675
  245. package/src/providers/integrations/commandcode/discovery.ts +117 -116
  246. package/src/providers/integrations/cursor/cursor-auth.ts +418 -418
  247. package/src/providers/integrations/cursor/cursor-quota.ts +309 -310
  248. package/src/providers/integrations/cursor/cursor.ts +11 -163
  249. package/src/providers/integrations/factory/factory-models.ts +3 -5
  250. package/src/providers/integrations/grok/grok-install-id.ts +2 -3
  251. package/src/providers/integrations/grok/grok.ts +6 -7
  252. package/src/providers/integrations/hermes.ts +14 -18
  253. package/src/providers/integrations/kiro/kiro-discovery.ts +1 -1
  254. package/src/providers/integrations/kiro/kiro.ts +1 -1
  255. package/src/providers/integrations/mistral.ts +6 -6
  256. package/src/providers/integrations/opencode/fingerprint.ts +3 -3
  257. package/src/providers/integrations/opencode/provider.ts +8 -8
  258. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -450
  259. package/src/providers/integrations/qoder/qoder-oauth.ts +388 -310
  260. package/src/providers/integrations/qoder/qoder.ts +884 -865
  261. package/src/providers/integrations/shared/connect.ts +6 -6
  262. package/src/providers/integrations/trae/trae-catalog.ts +1 -2
  263. package/src/providers/integrations/trae/trae.ts +2 -2
  264. package/src/providers/integrations/zcode/oauth.ts +6 -1
  265. package/src/providers/operations/account-health-service.ts +1528 -1523
  266. package/src/providers/operations/account-reset-service.ts +1 -1
  267. package/src/providers/operations/client-versions.ts +6 -6
  268. package/src/providers/operations/custom-cli-headers.ts +3 -4
  269. package/src/providers/operations/provider-catalog-seeder.ts +95 -95
  270. package/src/providers/operations/provider-catalog-service.ts +3 -4
  271. package/src/providers/provider-metadata.ts +4 -8
  272. package/src/providers/provider-registry.ts +6 -7
  273. package/src/providers/provider-usage.ts +1 -1
  274. package/src/security/abuse.ts +15 -10
  275. package/src/security/access-control.ts +144 -146
  276. package/src/security/admission/contracts.ts +11 -0
  277. package/src/security/admission/in-memory-store.ts +4 -3
  278. package/src/security/admission/lease-sweep.ts +6 -6
  279. package/src/security/admission/reasons.ts +5 -1
  280. package/src/security/admission/redis-store.ts +211 -210
  281. package/src/security/admission/service.ts +4 -0
  282. package/src/security/api-key-auth.ts +504 -504
  283. package/src/security/ip-boundary.ts +155 -15
  284. package/src/security/model-abuse.ts +366 -94
  285. package/src/security/outbound-headers.ts +3 -4
  286. package/src/shared/sleep.ts +4 -0
  287. package/src/transport/dispatch/attempt-finalize.ts +47 -5
  288. package/src/transport/dispatch/attempt-loop.ts +531 -530
  289. package/src/transport/dispatch/stream-bridge.ts +1 -3
  290. package/src/transport/dispatch/streaming/finalize.ts +1 -1
  291. package/src/transport/dispatch/streaming/stream.ts +5 -6
  292. package/src/transport/failure-policy.ts +162 -8
  293. package/src/transport/guards/auth-guard.ts +7 -4
  294. package/src/transport/guards/concurrency-guard.ts +0 -5
  295. package/src/transport/guards/error-lifecycle.ts +13 -6
  296. package/src/transport/guards/pipeline.ts +15 -3
  297. package/src/transport/guards/request-context.ts +8 -2
  298. package/src/transport/request/inflight.ts +5 -6
  299. package/src/transport/request/preparer.ts +2 -2
  300. package/src/transport/request/state.ts +12 -8
  301. package/src/transport/routing/admission.ts +4 -0
  302. package/src/transport/routing/route-catalog.ts +8 -1
  303. package/src/transport/routing/route-model.ts +46 -14
  304. package/src/transport/routing/router.ts +21 -14
  305. package/src/transport/surface/completion.ts +2 -3
  306. package/src/transport/surface/dialects.ts +5 -6
  307. package/src/transport/surface/responses/encode.ts +38 -0
  308. package/src/transport/surface/responses/parse.ts +4 -0
  309. package/src/transport/translation/capabilities.ts +2 -3
  310. package/src/transport/translation/quirks.ts +3 -3
  311. package/src/workers/buddy-growth-tasks.ts +5 -10
  312. package/src/workers/daily-checkin.ts +17 -23
  313. package/src/workers/quota-refresh-sweep.ts +26 -26
  314. package/dist/dashboard/assets/BannedPage-C2orBsI2.js.gz +0 -0
  315. package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js +0 -1
  316. package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js.gz +0 -0
  317. package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
  318. package/dist/dashboard/assets/CombosPage-Bsan4679.js +0 -1
  319. package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
  320. package/dist/dashboard/assets/ConsoleLogPage-CW-wPmJ6.js.gz +0 -0
  321. package/dist/dashboard/assets/CustomizationPage-0p8uX7vB.js.gz +0 -0
  322. package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js.gz +0 -0
  323. package/dist/dashboard/assets/LoginPage-BvzCx8Kr.js.gz +0 -0
  324. package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js +0 -1
  325. package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js.gz +0 -0
  326. package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js +0 -1
  327. package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js.gz +0 -0
  328. package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js +0 -3
  329. package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js.gz +0 -0
  330. package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js.gz +0 -0
  331. package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js +0 -7
  332. package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js.gz +0 -0
  333. package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js +0 -1
  334. package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js.gz +0 -0
  335. package/dist/dashboard/assets/SettingsPage-CS86ho5X.js +0 -1
  336. package/dist/dashboard/assets/SettingsPage-CS86ho5X.js.gz +0 -0
  337. package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
  338. package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
  339. package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
  340. package/dist/dashboard/assets/UsagePage-Cnae9SWA.js +0 -56
  341. package/dist/dashboard/assets/UsagePage-Cnae9SWA.js.gz +0 -0
  342. package/dist/dashboard/assets/api-keys-VY68Mhd1.js +0 -1
  343. package/dist/dashboard/assets/api-keys-VY68Mhd1.js.gz +0 -0
  344. package/dist/dashboard/assets/app-COlK95oi.css +0 -2
  345. package/dist/dashboard/assets/app-COlK95oi.css.gz +0 -0
  346. package/dist/dashboard/assets/assert-Chi7pCnt.js.gz +0 -0
  347. package/dist/dashboard/assets/badge-D4pQapKj.js.gz +0 -0
  348. package/dist/dashboard/assets/banner-DH6cg-_B.js.gz +0 -0
  349. package/dist/dashboard/assets/card-BKqlUOe4.js.gz +0 -0
  350. package/dist/dashboard/assets/cli-tools-fTgljTGA.js.gz +0 -0
  351. package/dist/dashboard/assets/clipboard-button-D5Ys_sM0.js.gz +0 -0
  352. package/dist/dashboard/assets/dialog-BYj-8WuK.js +0 -1
  353. package/dist/dashboard/assets/dialog-BYj-8WuK.js.gz +0 -0
  354. package/dist/dashboard/assets/drawer-CmXIZ6H1.js +0 -1
  355. package/dist/dashboard/assets/drawer-CmXIZ6H1.js.gz +0 -0
  356. package/dist/dashboard/assets/entry-CBg5zeu8.js +0 -1
  357. package/dist/dashboard/assets/entry-CBg5zeu8.js.gz +0 -0
  358. package/dist/dashboard/assets/entry-CQBr6U9c.js.gz +0 -0
  359. package/dist/dashboard/assets/entry-CgdSAzEV.js +0 -4
  360. package/dist/dashboard/assets/entry-CgdSAzEV.js.gz +0 -0
  361. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  362. package/dist/dashboard/assets/github-badge-BlptL8Uf.js.gz +0 -0
  363. package/dist/dashboard/assets/index-js6TJvdr.js.gz +0 -0
  364. package/dist/dashboard/assets/inline-DUWDSmDV.js.gz +0 -0
  365. package/dist/dashboard/assets/input-LVjgZq4f.js.gz +0 -0
  366. package/dist/dashboard/assets/layout-CPfxpsfy.js +0 -1
  367. package/dist/dashboard/assets/layout-CPfxpsfy.js.gz +0 -0
  368. package/dist/dashboard/assets/list-row-DupvLfDx.js.gz +0 -0
  369. package/dist/dashboard/assets/live-C4pEJUqI.js.gz +0 -0
  370. package/dist/dashboard/assets/model-bans-BRiuufkB.js.gz +0 -0
  371. package/dist/dashboard/assets/mutation-B4ROwk8L.js.gz +0 -0
  372. package/dist/dashboard/assets/numbers-YNcHvRY7.js +0 -2
  373. package/dist/dashboard/assets/numbers-YNcHvRY7.js.gz +0 -0
  374. package/dist/dashboard/assets/query-keys-gmjzC3RS.js.gz +0 -0
  375. package/dist/dashboard/assets/routing-C7tBT5o-.js.gz +0 -0
  376. package/dist/dashboard/assets/select-WUCXorXw.js.gz +0 -0
  377. package/dist/dashboard/assets/settings-C_zKl1GS.js.gz +0 -0
  378. package/dist/dashboard/assets/stack-_3O0F8Xn.js.gz +0 -0
  379. package/dist/dashboard/assets/state-BgTxHcJS.js.gz +0 -0
  380. package/dist/dashboard/assets/switch-DEjTtOWf.js.gz +0 -0
  381. package/dist/dashboard/assets/tokens-DDxMbSRL.js +0 -1
  382. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  383. package/dist/dashboard/assets/use-presence-BSZEBx7U.js +0 -1
  384. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  385. package/dist/dashboard/assets/useMutation-BWSC0UAS.js.gz +0 -0
  386. package/scripts/cli/command/admin.ts +0 -65
  387. package/scripts/cli/command/doctor.ts +0 -257
  388. package/scripts/cli/gateway/drain-token.ts +0 -78
  389. package/scripts/cli/gateway/instance-lock.ts +0 -105
  390. package/scripts/cli/gateway/stop.ts +0 -51
  391. package/scripts/cli/support/bun.ts +0 -27
  392. package/scripts/cli/support/version.ts +0 -37
  393. /package/dist/dashboard/assets/{app-DCQZRmHY.js → app-BIt446yE.js} +0 -0
  394. /package/dist/dashboard/assets/{app-DCQZRmHY.js.gz → app-BIt446yE.js.gz} +0 -0
  395. /package/scripts/cli/{ui/prompt.ts → prompt.ts} +0 -0
@@ -1,7 +1,6 @@
1
1
  // Cross-process cache coordination: a shared L2 in front of Postgres.
2
2
  //
3
- // Every hot read in the request path is cached in-process (see
4
- // `documentation/research/iomemory/01-request-path-census.md`). That is correct
3
+ // Every hot read in the request path is cached in-process. That is correct
5
4
  // for one process and wrong for several: `main.ts` sets `reusePort: true`, so N
6
5
  // gateway processes each hold their own copy. With no shared layer, a key
7
6
  // revoked through the console stays valid for up to a full TTL in every other
@@ -187,6 +187,7 @@ export interface ApiKeyStore {
187
187
  reorder(tenantId: string, keyIds: readonly string[]): Promise<void>;
188
188
  update(tenantId: string, keyId: string, patch: ApiKeyPatch): Promise<ApiKeyRecord | undefined>;
189
189
  revoke(tenantId: string, keyId: string, revokedAt: Date): Promise<boolean>;
190
+ remove(tenantId: string, keyId: string): Promise<boolean>;
190
191
  findActiveByHash?(hash: string): Promise<typeof apiKeys.$inferSelect | undefined>;
191
192
  findActiveById?(keyId: string): Promise<typeof apiKeys.$inferSelect | undefined>;
192
193
  sumChildrenConsumed?(parentKeyId: string): Promise<number>;
@@ -380,10 +381,6 @@ export class DrizzleApiKeyStore implements ApiKeyStore {
380
381
  .set({
381
382
  ...(patch.enabled !== undefined ? { enabled: patch.enabled } : {}),
382
383
  ...(patch.label !== undefined ? { label: patch.label } : {}),
383
- // Promotes a legacy default key: `isDefaultGatewayApiKey` recognizes
384
- // it by its canonical label, but recognition was never written back,
385
- // so the row stayed unflagged and a later rename — which changes the
386
- // only field that identifies it — dropped the default entirely.
387
384
  ...(patch.isDefault !== undefined ? { isDefault: patch.isDefault } : {}),
388
385
  ...(patch.keyHash !== undefined ? { keyHash: patch.keyHash } : {}),
389
386
  ...(patch.keyMode !== undefined ? { keyMode: patch.keyMode } : {}),
@@ -413,8 +410,8 @@ export class DrizzleApiKeyStore implements ApiKeyStore {
413
410
  : {}),
414
411
  // Scopes are validated once, in `validateApiKeyRequest`, against the
415
412
  // tenant scope set; the store writes what it is handed. Omitting the
416
- // field here meant an update was type-checked, accepted, and then
417
- // silently dropped — a grant like `routing:cli_mapping` could be
413
+ // field here means an update is type-checked, accepted, and then
414
+ // silently dropped — a grant like `routing:cli_mapping` can be
418
415
  // turned on and never stick.
419
416
  ...(patch.scopes === undefined ? {} : { scopes: patch.scopes }),
420
417
  })
@@ -453,6 +450,21 @@ export class DrizzleApiKeyStore implements ApiKeyStore {
453
450
  return true;
454
451
  });
455
452
  }
453
+ async remove(tenantId: string, keyId: string): Promise<boolean> {
454
+ return this.db.transaction(async (tx) => {
455
+ const rows = await tx
456
+ .delete(apiKeys)
457
+ .where(
458
+ and(
459
+ eq(apiKeys.tenantId, tenantId),
460
+ eq(apiKeys.id, keyId),
461
+ eq(apiKeys.isDefault, false),
462
+ ),
463
+ )
464
+ .returning({ id: apiKeys.id });
465
+ return rows.length > 0;
466
+ });
467
+ }
456
468
  async findActiveByHash(hash: string): Promise<typeof apiKeys.$inferSelect | undefined> {
457
469
  // The SQLite backend holds this lookup prepared. The builder path rebuilds
458
470
  // the query per call, which measured 60x the lookup itself, so the hot
@@ -308,9 +308,10 @@ export class DrizzleShareLinkStore implements ShareLinkStore {
308
308
  .set({
309
309
  tokenHash: input.tokenHash,
310
310
  tokenEncrypted: input.tokenEncrypted,
311
- // The slug is the address and survives a rotate, so an operator who
312
- // rotates does not invalidate every copy of the link they shared.
313
- ...(current.slug === null ? { slug: input.slug } : {}),
311
+ // Rotating the slug changes the public URL, so the old link stops resolving.
312
+ // Callers always generate a fresh slug via generateShareSlug() before calling
313
+ // create() with rotate: true.
314
+ slug: input.slug,
314
315
  expiresAt: input.expiresAt,
315
316
  lastViewedAt: null,
316
317
  })
@@ -1,237 +1,236 @@
1
- import { appendFile, mkdir, readdir, rm } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { sql } from "drizzle-orm";
4
- import { affectedRows, currentTimestamp, laterOf } from "../dialect";
5
- import type { CartethyiaDatabase } from "../db-handle";
6
- import {
7
- telemetryEvents,
8
- telemetryPayloads,
9
- telemetryUsageTotals,
10
- type TelemetryUsageIdentityType,
11
- } from "../schema";
12
- import { isGatewayError } from "../../observability/telemetry-status";
13
- import { TELEMETRY_EVENT_DIR } from "../../observability/event-log";
14
- import { dayEventFileName, utcDay } from "../../observability/event-day-index";
15
-
16
- interface UsageTotalDelta {
17
- readonly tenantId: string;
18
- readonly identityType: TelemetryUsageIdentityType;
19
- readonly entityId: string;
20
- requests: number;
21
- errors: number;
22
- inputTokens: number;
23
- outputTokens: number;
24
- }
25
-
26
- function addUsageDelta(
27
- deltas: Map<string, UsageTotalDelta>,
28
- row: typeof telemetryEvents.$inferInsert,
29
- identityType: TelemetryUsageIdentityType,
30
- entityId: string | null | undefined,
31
- ): void {
32
- if (entityId == null) return;
33
- const key = `${row.tenantId}\0${identityType}\0${entityId}`;
34
- let delta = deltas.get(key);
35
- if (!delta) {
36
- delta = {
37
- tenantId: row.tenantId,
38
- identityType,
39
- entityId,
40
- requests: 0,
41
- errors: 0,
42
- inputTokens: 0,
43
- outputTokens: 0,
44
- };
45
- deltas.set(key, delta);
46
- }
47
- delta.requests += 1;
48
- // Same definition as every read-side error count, including the HTTP-status
49
- // exclusions (404/499/503/504): this rollup is durable, so a request counted here
50
- // stays counted after its raw row is pruned and cannot be corrected later.
51
- if (isGatewayError(row.status, row.httpStatus)) delta.errors += 1;
52
- delta.inputTokens += row.inputTokens ?? 0;
53
- delta.outputTokens += row.outputTokens ?? 0;
54
- }
55
-
56
- function aggregateUsage(rows: readonly (typeof telemetryEvents.$inferInsert)[]) {
57
- const deltas = new Map<string, UsageTotalDelta>();
58
- for (const row of rows) {
59
- addUsageDelta(deltas, row, "account", row.accountId);
60
- addUsageDelta(deltas, row, "api_key", row.apiKeyId);
61
- }
62
- return [...deltas.values()];
63
- }
64
-
65
- /**
66
- * Database boundary for durable telemetry writes and bounded retention cleanup.
67
- * The observability buffer owns queueing and retry policy; this store owns table
68
- * operations, lifetime aggregates, and retention.
69
- */
70
-
71
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
72
-
73
- /**
74
- * Deletes whole days past the retention cutoff: the event file and its index
75
- * together.
76
- *
77
- * The day is the retention unit now, so both go at once. Removing one without
78
- * the other is the failure this avoids — an index with no events reports totals
79
- * for requests that no longer exist, and events with no index are invisible to
80
- * every usage read.
81
- *
82
- * The cutoff is a whole day, so today's file — still being appended to — is
83
- * never a candidate. It used to compare filenames hour by hour, which for a day
84
- * file compared the `.events` suffix against an hour stamp and could drop the
85
- * open file.
86
- */
87
- async function pruneEventFiles(before: Date): Promise<void> {
88
- const files = await readdir(TELEMETRY_EVENT_DIR).catch(() => [] as string[]);
89
- const cutoffDay = before.toISOString().slice(0, 10);
90
- for (const file of files) {
91
- const day = file.slice(0, 10);
92
- if (day.length !== 10 || day >= cutoffDay) continue;
93
- if (!file.endsWith(".events.jsonl") && !file.endsWith(".index.json")) continue;
94
- await rm(join(TELEMETRY_EVENT_DIR, file), { force: true });
95
- }
96
- }
97
-
98
- /**
99
- * Appends one JSON line per event to the day's event file.
100
- *
101
- * One file per UTC day, appended to and never rewritten while the day is open.
102
- * It used to be one file per `windowMinutes` window — 96 a day at the default —
103
- * which the reader then had to find, open and parse one by one on every usage
104
- * query. Which of those files was still being written had to be inferred from
105
- * the name, and that inference is what let the open one be deleted mid-write.
106
- *
107
- * A single growing file has no such ambiguity, and the day index tracks how much
108
- * of it has been counted by byte offset.
109
- */
110
- async function appendEvents(rows: readonly (typeof telemetryEvents.$inferInsert)[]): Promise<void> {
111
- await mkdir(TELEMETRY_EVENT_DIR, { recursive: true });
112
- const file = join(TELEMETRY_EVENT_DIR, dayEventFileName(utcDay(new Date())));
113
- const newline = String.fromCharCode(10);
114
- const body = rows.map((row) => JSON.stringify(row)).join(newline) + newline;
115
- await appendFile(file, body, "utf8");
116
- }
117
-
118
- export class DrizzleTelemetryStore {
119
- constructor(private readonly db: CartethyiaDatabase) {}
120
-
121
- /** Inserts events and their durable identity aggregates atomically. */
122
- async insertEvents(rows: Array<typeof telemetryEvents.$inferInsert>): Promise<void> {
123
- if (rows.length === 0) return;
124
- // The file accepts anything, so a malformed row would be written and then
125
- // retried forever without ever failing. Rejecting it here keeps the buffer's
126
- // drop-and-count contract: one bad row fails the batch, once.
127
- for (const row of rows) {
128
- if (!UUID_RE.test(row.tenantId)) {
129
- throw new Error("telemetry event rejected: tenantId is not a uuid");
130
- }
131
- }
132
- const deltas = aggregateUsage(rows);
133
- // The per-request row goes to an append-only file, not to Postgres. The
134
- // database keeps only the durable aggregate below, so a request never
135
- // adds a row to the largest table. Readers of recent requests read the
136
- // file; readers of long-term totals read the aggregate.
137
- await this.db.transaction(async (tx) => {
138
- if (deltas.length === 0) return;
139
- const now = new Date();
140
- await tx
141
- .insert(telemetryUsageTotals)
142
- .values(deltas.map((delta) => ({ ...delta, lastUsedAt: now })))
143
- .onConflictDoUpdate({
144
- target: [
145
- telemetryUsageTotals.tenantId,
146
- telemetryUsageTotals.identityType,
147
- telemetryUsageTotals.entityId,
148
- ],
149
- set: {
150
- requests: sql`${telemetryUsageTotals.requests} + excluded.requests`,
151
- errors: sql`${telemetryUsageTotals.errors} + excluded.errors`,
152
- inputTokens: sql`${telemetryUsageTotals.inputTokens} + excluded.input_tokens`,
153
- outputTokens: sql`${telemetryUsageTotals.outputTokens} + excluded.output_tokens`,
154
- lastUsedAt: laterOf(sql`${telemetryUsageTotals.lastUsedAt}`, sql`excluded.last_used_at`),
155
- updatedAt: currentTimestamp(),
156
- },
157
- });
158
- });
159
- // After the aggregate commits, so a rejected batch is retried once, not
160
- // appended once per attempt. The file is the record of requests whose
161
- // totals were actually counted.
162
- await appendEvents(rows);
163
- }
164
-
165
- /** Inserts one redacted payload and returns its generated identity. */
166
- async insertPayload(
167
- row: typeof telemetryPayloads.$inferInsert,
168
- ): Promise<{ id: string; expiresAt: Date }> {
169
- const [inserted] = await this.db
170
- .insert(telemetryPayloads)
171
- .values(row)
172
- .returning({ id: telemetryPayloads.id, expiresAt: telemetryPayloads.expiresAt });
173
- if (!inserted) throw new Error("payload capture insert returned no row");
174
- return inserted;
175
- }
176
-
177
- /**
178
- * Deletes at most one bounded batch of expired payloads in a single
179
- * statement, so concurrent sweeps atomically claim disjoint rows and the
180
- * returned count matches the rows actually deleted.
181
- */
182
- async deleteExpiredPayloads(batchSize = 5000, before = new Date()): Promise<number> {
183
- const result = await this.db.execute(sql`
184
- DELETE FROM ${telemetryPayloads}
185
- WHERE ${telemetryPayloads.id} IN (
186
- SELECT ${telemetryPayloads.id} FROM ${telemetryPayloads}
187
- WHERE ${telemetryPayloads.expiresAt} < ${before}
188
- ORDER BY ${telemetryPayloads.expiresAt}
189
- LIMIT ${batchSize}
190
- )
191
- `);
192
- return affectedRows(result);
193
- }
194
-
195
- /**
196
- * Deletes at most one bounded batch of aged events in a single statement, so
197
- * concurrent sweeps atomically claim disjoint rows and the returned count
198
- * matches the rows actually deleted. Ordered by `created_at`, which
199
- * `idx_telemetry_created_at` backs.
200
- */
201
- async deleteExpiredEvents(batchSize = 5000, before: Date): Promise<number> {
202
- const result = await this.db.execute(sql`
203
- DELETE FROM ${telemetryEvents}
204
- WHERE ${telemetryEvents.id} IN (
205
- SELECT ${telemetryEvents.id} FROM ${telemetryEvents}
206
- WHERE ${telemetryEvents.createdAt} < ${before}
207
- ORDER BY ${telemetryEvents.createdAt}
208
- LIMIT ${batchSize}
209
- )
210
- `);
211
- return affectedRows(result);
212
- }
213
-
214
- /**
215
- * Deletes telemetry older than the supplied cutoff. Payloads follow their
216
- * `expires_at` contract (15 minutes per row) rather than capture time.
217
- *
218
- * Events are pruned in bounded batches rather than one `DELETE`, because
219
- * `telemetry_events` is the highest-volume table and a single statement
220
- * covering every aged row grows without limit: it holds locks and WAL for the
221
- * whole sweep, and on a deployment that has run for months it can exceed the
222
- * pool's `statement_timeout` and be cancelled, so the sweep never converges
223
- * and retention silently stops working. Each batch commits independently, so
224
- * progress survives a cancel or a restart. The loop is bounded so one pass
225
- * cannot run unbounded — a backlog drains over successive sweeps — mirroring
226
- * the payload sweeper's `cleanupExpired`.
227
- */
228
- async pruneTelemetry(before: Date): Promise<void> {
229
- await pruneEventFiles(before);
230
- const MAX_BATCHES_PER_SWEEP = 40;
231
- for (let batch = 0; batch < MAX_BATCHES_PER_SWEEP; batch += 1) {
232
- const deleted = await this.deleteExpiredEvents(5000, before);
233
- if (deleted < 5000) break;
234
- }
235
- await this.deleteExpiredPayloads(5000, before);
236
- }
237
- }
1
+ import { appendFile, mkdir, readdir, rm } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { sql } from "drizzle-orm";
4
+ import { affectedRows, currentTimestamp, laterOf } from "../dialect";
5
+ import type { CartethyiaDatabase } from "../db-handle";
6
+ import {
7
+ telemetryEvents,
8
+ telemetryPayloads,
9
+ telemetryUsageTotals,
10
+ type TelemetryUsageIdentityType,
11
+ } from "../schema";
12
+ import { isGatewayError } from "../../observability/telemetry-status";
13
+ import { TELEMETRY_EVENT_DIR } from "../../observability/event-log";
14
+ import { dayEventFileName, utcDay } from "../../observability/event-day-index";
15
+
16
+ interface UsageTotalDelta {
17
+ readonly tenantId: string;
18
+ readonly identityType: TelemetryUsageIdentityType;
19
+ readonly entityId: string;
20
+ requests: number;
21
+ errors: number;
22
+ inputTokens: number;
23
+ outputTokens: number;
24
+ }
25
+
26
+ function addUsageDelta(
27
+ deltas: Map<string, UsageTotalDelta>,
28
+ row: typeof telemetryEvents.$inferInsert,
29
+ identityType: TelemetryUsageIdentityType,
30
+ entityId: string | null | undefined,
31
+ ): void {
32
+ if (entityId == null) return;
33
+ const key = `${row.tenantId}\0${identityType}\0${entityId}`;
34
+ let delta = deltas.get(key);
35
+ if (!delta) {
36
+ delta = {
37
+ tenantId: row.tenantId,
38
+ identityType,
39
+ entityId,
40
+ requests: 0,
41
+ errors: 0,
42
+ inputTokens: 0,
43
+ outputTokens: 0,
44
+ };
45
+ deltas.set(key, delta);
46
+ }
47
+ delta.requests += 1;
48
+ // Same definition as every read-side error count, including the HTTP-status
49
+ // exclusions (404/499/503/504): this rollup is durable, so a request counted here
50
+ // stays counted after its raw row is pruned and cannot be corrected later.
51
+ if (isGatewayError(row.status, row.httpStatus)) delta.errors += 1;
52
+ delta.inputTokens += row.inputTokens ?? 0;
53
+ delta.outputTokens += row.outputTokens ?? 0;
54
+ }
55
+
56
+ function aggregateUsage(rows: readonly (typeof telemetryEvents.$inferInsert)[]) {
57
+ const deltas = new Map<string, UsageTotalDelta>();
58
+ for (const row of rows) {
59
+ addUsageDelta(deltas, row, "account", row.accountId);
60
+ addUsageDelta(deltas, row, "api_key", row.apiKeyId);
61
+ }
62
+ return [...deltas.values()];
63
+ }
64
+
65
+ /**
66
+ * Database boundary for durable telemetry writes and bounded retention cleanup.
67
+ * The observability buffer owns queueing and retry policy; this store owns table
68
+ * operations, lifetime aggregates, and retention.
69
+ */
70
+
71
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
72
+
73
+ /**
74
+ * Deletes whole days past the retention cutoff: the event file and its index
75
+ * together.
76
+ *
77
+ * The day is the retention unit now, so both go at once. Removing one without
78
+ * the other is the failure this avoids — an index with no events reports totals
79
+ * for requests that no longer exist, and events with no index are invisible to
80
+ * every usage read.
81
+ *
82
+ * The cutoff is a whole day, so today's file — still being appended to — is
83
+ * never a candidate. Comparing filenames hour by hour would match a day file's
84
+ * `.events` suffix against an hour stamp and could drop the open file.
85
+ */
86
+ async function pruneEventFiles(before: Date): Promise<void> {
87
+ const files = await readdir(TELEMETRY_EVENT_DIR).catch(() => [] as string[]);
88
+ const cutoffDay = before.toISOString().slice(0, 10);
89
+ for (const file of files) {
90
+ const day = file.slice(0, 10);
91
+ if (day.length !== 10 || day >= cutoffDay) continue;
92
+ if (!file.endsWith(".events.jsonl") && !file.endsWith(".index.json")) continue;
93
+ await rm(join(TELEMETRY_EVENT_DIR, file), { force: true });
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Appends one JSON line per event to the day's event file.
99
+ *
100
+ * One file per UTC day, appended to and never rewritten while the day is open.
101
+ * A file per `windowMinutes` window — 96 a day at the default — would have to be
102
+ * found, opened and parsed one by one on every usage query, and which of those
103
+ * files was still being written could only be inferred from the name, which is
104
+ * what lets the open one be deleted mid-write.
105
+ *
106
+ * A single growing file has no such ambiguity, and the day index tracks how much
107
+ * of it has been counted by byte offset.
108
+ */
109
+ async function appendEvents(rows: readonly (typeof telemetryEvents.$inferInsert)[]): Promise<void> {
110
+ await mkdir(TELEMETRY_EVENT_DIR, { recursive: true });
111
+ const file = join(TELEMETRY_EVENT_DIR, dayEventFileName(utcDay(new Date())));
112
+ const newline = String.fromCharCode(10);
113
+ const body = rows.map((row) => JSON.stringify(row)).join(newline) + newline;
114
+ await appendFile(file, body, "utf8");
115
+ }
116
+
117
+ export class DrizzleTelemetryStore {
118
+ constructor(private readonly db: CartethyiaDatabase) {}
119
+
120
+ /** Inserts events and their durable identity aggregates atomically. */
121
+ async insertEvents(rows: Array<typeof telemetryEvents.$inferInsert>): Promise<void> {
122
+ if (rows.length === 0) return;
123
+ // The file accepts anything, so a malformed row would be written and then
124
+ // retried forever without ever failing. Rejecting it here keeps the buffer's
125
+ // drop-and-count contract: one bad row fails the batch, once.
126
+ for (const row of rows) {
127
+ if (!UUID_RE.test(row.tenantId)) {
128
+ throw new Error("telemetry event rejected: tenantId is not a uuid");
129
+ }
130
+ }
131
+ const deltas = aggregateUsage(rows);
132
+ // The per-request row goes to an append-only file, not to Postgres. The
133
+ // database keeps only the durable aggregate below, so a request never
134
+ // adds a row to the largest table. Readers of recent requests read the
135
+ // file; readers of long-term totals read the aggregate.
136
+ await this.db.transaction(async (tx) => {
137
+ if (deltas.length === 0) return;
138
+ const now = new Date();
139
+ await tx
140
+ .insert(telemetryUsageTotals)
141
+ .values(deltas.map((delta) => ({ ...delta, lastUsedAt: now })))
142
+ .onConflictDoUpdate({
143
+ target: [
144
+ telemetryUsageTotals.tenantId,
145
+ telemetryUsageTotals.identityType,
146
+ telemetryUsageTotals.entityId,
147
+ ],
148
+ set: {
149
+ requests: sql`${telemetryUsageTotals.requests} + excluded.requests`,
150
+ errors: sql`${telemetryUsageTotals.errors} + excluded.errors`,
151
+ inputTokens: sql`${telemetryUsageTotals.inputTokens} + excluded.input_tokens`,
152
+ outputTokens: sql`${telemetryUsageTotals.outputTokens} + excluded.output_tokens`,
153
+ lastUsedAt: laterOf(sql`${telemetryUsageTotals.lastUsedAt}`, sql`excluded.last_used_at`),
154
+ updatedAt: currentTimestamp(),
155
+ },
156
+ });
157
+ });
158
+ // After the aggregate commits, so a rejected batch is retried once, not
159
+ // appended once per attempt. The file is the record of requests whose
160
+ // totals were actually counted.
161
+ await appendEvents(rows);
162
+ }
163
+
164
+ /** Inserts one redacted payload and returns its generated identity. */
165
+ async insertPayload(
166
+ row: typeof telemetryPayloads.$inferInsert,
167
+ ): Promise<{ id: string; expiresAt: Date }> {
168
+ const [inserted] = await this.db
169
+ .insert(telemetryPayloads)
170
+ .values(row)
171
+ .returning({ id: telemetryPayloads.id, expiresAt: telemetryPayloads.expiresAt });
172
+ if (!inserted) throw new Error("payload capture insert returned no row");
173
+ return inserted;
174
+ }
175
+
176
+ /**
177
+ * Deletes at most one bounded batch of expired payloads in a single
178
+ * statement, so concurrent sweeps atomically claim disjoint rows and the
179
+ * returned count matches the rows actually deleted.
180
+ */
181
+ async deleteExpiredPayloads(batchSize = 5000, before = new Date()): Promise<number> {
182
+ const result = await this.db.execute(sql`
183
+ DELETE FROM ${telemetryPayloads}
184
+ WHERE ${telemetryPayloads.id} IN (
185
+ SELECT ${telemetryPayloads.id} FROM ${telemetryPayloads}
186
+ WHERE ${telemetryPayloads.expiresAt} < ${before}
187
+ ORDER BY ${telemetryPayloads.expiresAt}
188
+ LIMIT ${batchSize}
189
+ )
190
+ `);
191
+ return affectedRows(result);
192
+ }
193
+
194
+ /**
195
+ * Deletes at most one bounded batch of aged events in a single statement, so
196
+ * concurrent sweeps atomically claim disjoint rows and the returned count
197
+ * matches the rows actually deleted. Ordered by `created_at`, which
198
+ * `idx_telemetry_created_at` backs.
199
+ */
200
+ async deleteExpiredEvents(batchSize = 5000, before: Date): Promise<number> {
201
+ const result = await this.db.execute(sql`
202
+ DELETE FROM ${telemetryEvents}
203
+ WHERE ${telemetryEvents.id} IN (
204
+ SELECT ${telemetryEvents.id} FROM ${telemetryEvents}
205
+ WHERE ${telemetryEvents.createdAt} < ${before}
206
+ ORDER BY ${telemetryEvents.createdAt}
207
+ LIMIT ${batchSize}
208
+ )
209
+ `);
210
+ return affectedRows(result);
211
+ }
212
+
213
+ /**
214
+ * Deletes telemetry older than the supplied cutoff. Payloads follow their
215
+ * `expires_at` contract (15 minutes per row) rather than capture time.
216
+ *
217
+ * Events are pruned in bounded batches rather than one `DELETE`, because
218
+ * `telemetry_events` is the highest-volume table and a single statement
219
+ * covering every aged row grows without limit: it holds locks and WAL for the
220
+ * whole sweep, and on a deployment that has run for months it can exceed the
221
+ * pool's `statement_timeout` and be cancelled, so the sweep never converges
222
+ * and retention silently stops working. Each batch commits independently, so
223
+ * progress survives a cancel or a restart. The loop is bounded so one pass
224
+ * cannot run unbounded — a backlog drains over successive sweeps — mirroring
225
+ * the payload sweeper's `cleanupExpired`.
226
+ */
227
+ async pruneTelemetry(before: Date): Promise<void> {
228
+ await pruneEventFiles(before);
229
+ const MAX_BATCHES_PER_SWEEP = 40;
230
+ for (let batch = 0; batch < MAX_BATCHES_PER_SWEEP; batch += 1) {
231
+ const deleted = await this.deleteExpiredEvents(5000, before);
232
+ if (deleted < 5000) break;
233
+ }
234
+ await this.deleteExpiredPayloads(5000, before);
235
+ }
236
+ }
@@ -308,6 +308,13 @@ export type ContentPart =
308
308
  /** Responses reasoning summary block index, preserved across surfaces. */
309
309
  summary_index?: number;
310
310
  signature?: string;
311
+ /**
312
+ * Responses item id the `encrypted_content` was issued for. The upstream
313
+ * binds encrypted reasoning to its item and rejects a replay whose item
314
+ * id does not match, so the id travels with the payload it belongs to
315
+ * rather than being re-derived at request time.
316
+ */
317
+ item_id?: string;
311
318
  encrypted_content?: string;
312
319
  }
313
320
  | { kind: "extension"; name: string; payload: unknown };
@@ -596,9 +603,10 @@ export function hasEncryptedReasoning(parts: readonly ContentPart[]): boolean {
596
603
  * Single-pass summary of a request's content parts.
597
604
  *
598
605
  * `deriveRequiredCapabilities` + `assessTranslation` run on every candidate
599
- * preflight and previously each flattened the whole request (`allParts`) and
600
- * then scanned the copy up to ~6 times. This walks the messages once, builds
601
- * no intermediate array, and answers every predicate the pipeline needs.
606
+ * preflight, and flattening the whole request (`allParts`) in each and then
607
+ * scanning the copy up to ~6 times is the cost this avoids. It walks the
608
+ * messages once, builds no intermediate array, and answers every predicate the
609
+ * pipeline needs.
602
610
  * Results are memoized per request object in a `WeakMap`, so the preflight +
603
611
  * translate passes over one request share a single summary and entries are
604
612
  * collected with the request itself — no manual invalidation, no leak.