cartethyia 2.7.0 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +107 -106
- package/README.md +25 -2
- package/dist/dashboard/assets/{BannedPage-P76QLGJA.js → BannedPage-D-J75hBM.js} +1 -1
- package/dist/dashboard/assets/BannedPage-D-J75hBM.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-DdBggJS9.js → CliToolDetailPage-CVe6RaBH.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-CVe6RaBH.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-aN1iXhhP.js → CliToolsPage-Dd8ZSamp.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-Dd8ZSamp.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-DE-F1nxZ.js → CombosPage-QoJ6foxo.js} +1 -1
- package/dist/dashboard/assets/CombosPage-QoJ6foxo.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DpNRay4C.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-DpNRay4C.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-3CUxMy2X.js → CustomizationPage-CKUFTcW4.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-CKUFTcW4.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DubdQcvX.js +1 -0
- package/dist/dashboard/assets/LoginPage-DubdQcvX.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-DKVslqG3.js → ModelPicker-BH8Tehdi.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js +1 -0
- package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js +4 -0
- package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js +9 -0
- package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js.gz +0 -0
- package/dist/dashboard/assets/{QuotaPage-BjldQwIU.js → QuotaPage-ATpaxrbl.js} +1 -1
- package/dist/dashboard/assets/QuotaPage-ATpaxrbl.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-DUIoLRxk.js → SettingsPage-CETophpx.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-CETophpx.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-BdgECjqq.js +1 -0
- package/dist/dashboard/assets/SetupPage-BdgECjqq.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-fI7JkdIB.js → SortableList-CAK-EN9z.js} +1 -1
- package/dist/dashboard/assets/SortableList-CAK-EN9z.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-CHVZqskZ.js → StudioPage-B7W2_RhT.js} +3 -3
- package/dist/dashboard/assets/StudioPage-B7W2_RhT.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-6T4EFDjW.js +56 -0
- package/dist/dashboard/assets/UsagePage-6T4EFDjW.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-DFY_fZU4.js → api-keys-CUi6UXsR.js} +1 -1
- package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
- package/dist/dashboard/assets/app-KvsUJUMF.css +2 -0
- package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
- package/dist/dashboard/assets/{assert-CwdnZARt.js → assert-67xXH5rf.js} +1 -1
- package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
- package/dist/dashboard/assets/{badge-Dr-uf0Au.js → badge-DC2L43Mj.js} +1 -1
- package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BddmMdoS.js → banner-BBVcb0J3.js} +1 -1
- package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
- package/dist/dashboard/assets/{card-CK0PXWlX.js → card-Cca1XC6G.js} +1 -1
- package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-CcB6mGh2.js → cli-tools-KvsWa1Jl.js} +1 -1
- package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DGb2P9Yx.js → clipboard-button-DUzEbynq.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-LkfuL50u.js → dialog-DFvWhwTa.js} +1 -1
- package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-ZbEZ_84c.js → drawer-CFM2sMX1.js} +1 -1
- package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CWBY5A0f.js → entry-BLBLxX0h.js} +3 -3
- package/dist/dashboard/assets/entry-BLBLxX0h.js.gz +0 -0
- package/dist/dashboard/assets/{entry-BJfsVMvA.js → entry-Bqy8RFvM.js} +1 -1
- package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CvC1-Zqx.js → entry-_xHo5x6r.js} +1 -1
- package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
- package/dist/dashboard/assets/formatters-mpBPqAQ8.js +1 -0
- package/dist/dashboard/assets/formatters-mpBPqAQ8.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-BxpznFLt.js → github-badge-C9e8Ts7e.js} +1 -1
- package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
- package/dist/dashboard/assets/{index-CDzP3ovJ.js → index-D6QzEC_n.js} +2 -2
- package/dist/dashboard/assets/index-D6QzEC_n.js.gz +0 -0
- package/dist/dashboard/assets/{inline-Ae13MQ5w.js → inline-BFUcvllj.js} +1 -1
- package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
- package/dist/dashboard/assets/{input-D23fo7cp.js → input-C_ILAvBb.js} +1 -1
- package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
- package/dist/dashboard/assets/{layout-CSsxjj5P.js → layout-CBIyT6Rt.js} +1 -1
- package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-dmbwZMDY.js → list-row-ebJ_-Nsb.js} +1 -1
- package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
- package/dist/dashboard/assets/{live-BG1oUsZZ.js → live-D0mm--ME.js} +1 -1
- package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-DSF-e4ho.js → mutation-sMD965oO.js} +1 -1
- package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-Bm_aECY7.js → numbers-BO9dKaFr.js} +1 -1
- package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
- package/dist/dashboard/assets/providers/inception.svg +5 -0
- package/dist/dashboard/assets/providers/inception.svg.gz +0 -0
- package/dist/dashboard/assets/{query-keys-CF56nYIV.js → query-keys-CfL_Zdoh.js} +1 -1
- package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
- package/dist/dashboard/assets/{routing-CeA71n7G.js → routing-BnIpi1IJ.js} +1 -1
- package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
- package/dist/dashboard/assets/{select-KIT0rC6G.js → select-Cj5Nw43g.js} +1 -1
- package/dist/dashboard/assets/select-Cj5Nw43g.js.gz +0 -0
- package/dist/dashboard/assets/{settings-B_2gkePf.js → settings-BAFhDgX8.js} +1 -1
- package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
- package/dist/dashboard/assets/{stack-D7WOSzcX.js → stack-EMrOMt4I.js} +1 -1
- package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
- package/dist/dashboard/assets/{state-BBxtKZOQ.js → state-CWoo7ty5.js} +1 -1
- package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DNS_D5Ea.js → switch-CCjT6IkF.js} +1 -1
- package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BgoxdoMk.js → useMutation-DjkLHUcE.js} +1 -1
- package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +54 -53
- package/migrations/full-postgres/0024_add_pool_provider_cooldowns.sql +12 -0
- package/migrations/full-postgres/0025_add_pool_random_strategy.sql +3 -0
- package/migrations/lite-sqlite3/0024_add_pool_provider_cooldowns.sql +12 -0
- package/migrations/lite-sqlite3/0025_add_pool_random_strategy.sql +20 -0
- package/package.json +96 -96
- package/scripts/cli/console-menu.ts +5 -2
- package/scripts/cli/tray.ts +21 -6
- package/src/boot/app.ts +684 -714
- package/src/boot/dependencies.ts +65 -62
- package/src/config/cooldowns.ts +16 -0
- package/src/config/providers.ts +13 -1
- package/src/config/resources.ts +70 -70
- package/src/config/security.ts +37 -37
- package/src/config/spec.ts +371 -358
- package/src/console/console-router.ts +233 -225
- package/src/console/domain-registration.ts +398 -381
- package/src/console/providers/catalog/catalog-projections.ts +2 -6
- package/src/console/providers/catalog/contracts.ts +19 -0
- package/src/console/providers/catalog/provider-operations.ts +38 -0
- package/src/console/providers/catalog/routes.ts +14 -0
- package/src/console/providers/catalog/store-helpers.ts +326 -0
- package/src/console/providers/catalog/store.ts +1292 -1563
- package/src/console/quota/account-quota-global.ts +2 -2
- package/src/console/quota/account-quota-shared.ts +1 -1
- package/src/console/quota/account-quota-tenant.ts +35 -12
- package/src/console/quota/quota-view.ts +2 -2
- package/src/console/routing/pools/contracts.ts +41 -1
- package/src/console/routing/pools/operations.ts +183 -4
- package/src/console/routing/pools/relay-deploy.ts +17 -4
- package/src/console/routing/pools/routes.ts +30 -0
- package/src/console/routing/pools/store.ts +98 -8
- package/src/main.ts +183 -167
- package/src/network/outbound-fetch.ts +226 -365
- package/src/network/pool/agent.ts +8 -0
- package/src/network/pool/pool-state.ts +7 -0
- package/src/network/pool/selector.ts +110 -2
- package/src/network/pool-cooldown-store.ts +252 -0
- package/src/network/pool-health-machine.ts +5 -3
- package/src/network/pool-health.ts +36 -9
- package/src/observability/event-query.ts +57 -0
- package/src/observability/metrics.ts +467 -467
- package/src/observability/runtime-metrics.ts +269 -269
- package/src/persistence/health-event-flush.ts +2 -0
- package/src/persistence/schema.ts +19 -0
- package/src/persistence/usage-flush.ts +8 -3
- package/src/protocol/canonical-model.ts +1 -1
- package/src/protocol/dsml.ts +377 -0
- package/src/protocol/gateway-error.ts +232 -232
- package/src/{providers/provider-reasoning.ts → protocol/reasoning.ts} +10 -9
- package/src/protocol/request/chat.ts +7 -7
- package/src/protocol/request/codex.ts +2 -2
- package/src/protocol/request/gemini.ts +1 -1
- package/src/protocol/request/messages.ts +6 -6
- package/src/protocol/request/responses.ts +7 -7
- package/src/protocol/response/dsml.ts +12 -133
- package/src/{transport → protocol}/translation/attachment-integrity.ts +2 -2
- package/src/{transport → protocol}/translation/cache-controls.ts +1 -1
- package/src/{transport → protocol}/translation/capabilities.ts +2 -2
- package/src/{transport → protocol}/translation/normalize.ts +1 -1
- package/src/{transport → protocol}/translation/quirks.ts +1 -1
- package/src/{transport → protocol}/translation/search.ts +1 -1
- package/src/{transport → protocol}/translation/thinking.ts +23 -1
- package/src/{transport → protocol}/translation/tool-id.ts +1 -1
- package/src/{transport → protocol}/translation/tool-repair.ts +2 -2
- package/src/protocol/upstream-parsing.ts +176 -0
- package/src/providers/adapters/compatible-adapter.ts +11 -2
- package/src/providers/authentication/oauth-refresh-service.ts +9 -5
- package/src/providers/default-registry.ts +21 -5
- package/src/providers/discovery/probe-phases.ts +16 -3
- package/src/providers/discovery/probing-service.ts +4 -1
- package/src/providers/integrations/anthropic.ts +1 -1
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -4
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +141 -8
- package/src/providers/integrations/buddy/buddy-checkin.ts +5 -1
- package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -82
- package/src/providers/integrations/buddy/buddy-identity.ts +25 -0
- package/src/providers/integrations/buddy/buddy-server-tools.ts +240 -0
- package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +36 -0
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +6 -31
- package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -0
- package/src/providers/integrations/buddy/codebuddy/shared.ts +19 -4
- package/src/providers/integrations/buddy/turing-device-token.ts +262 -0
- package/src/providers/integrations/buddy/workbuddy/provider.ts +2 -0
- package/src/providers/integrations/buddy/workbuddy/shared.ts +56 -3
- package/src/providers/integrations/claude/claude.ts +1 -1
- package/src/providers/integrations/cloudflareai/cloudflareai.ts +1 -1
- package/src/providers/integrations/codex/codex-errors.ts +1 -1
- package/src/providers/integrations/codex/codex.ts +1 -1
- package/src/providers/integrations/factory/factory.ts +1 -1
- package/src/providers/integrations/github/github.ts +1 -1
- package/src/providers/integrations/grok/grok.ts +1 -1
- package/src/providers/integrations/inception/provider.ts +91 -0
- package/src/providers/integrations/inferhub.ts +1 -1
- package/src/providers/integrations/kiro/kiro-request.ts +1 -1
- package/src/providers/integrations/mistral.ts +193 -19
- package/src/providers/integrations/opencode/fingerprint.ts +19 -2
- package/src/providers/integrations/opencode/prompt.ts +111 -0
- package/src/providers/integrations/opencode/provider.ts +101 -23
- package/src/providers/integrations/opencode/tool-definitions.ts +4 -4
- package/src/providers/integrations/qoder/qoder-checkin.ts +303 -0
- package/src/providers/integrations/qoder/qoder.ts +1641 -1458
- package/src/providers/integrations/shared/install-id.ts +2 -1
- package/src/providers/integrations/trae/trae-cache.ts +110 -0
- package/src/providers/integrations/trae/trae-oauth.ts +29 -4
- package/src/providers/integrations/trae/trae-protocol.ts +2 -1
- package/src/providers/integrations/trae/trae.ts +106 -23
- package/src/providers/operations/account-error-classification.ts +1660 -0
- package/src/providers/operations/account-health-service.ts +621 -2097
- package/src/providers/operations/account-reset-service.ts +2 -0
- package/src/providers/provider-contracts.ts +0 -2
- package/src/providers/provider-metadata.ts +25 -4
- package/src/providers/provider-probe.ts +2 -2
- package/src/{console → providers}/quota/cache.ts +1 -1
- package/src/{console → providers}/quota/refresh.ts +5 -5
- package/src/transport/dispatch/attempt-finalize.ts +114 -12
- package/src/transport/dispatch/attempt-loop.ts +3 -0
- package/src/transport/dispatch/empty-stream.ts +224 -217
- package/src/transport/dispatch/generation.ts +37 -55
- package/src/transport/dispatch/handler-deps.ts +32 -0
- package/src/transport/dispatch/leases.ts +9 -4
- package/src/transport/dispatch/proxy-request.ts +6 -30
- package/src/transport/dispatch/responses-compact.ts +22 -43
- package/src/transport/dispatch/streaming/finalize.ts +3 -0
- package/src/transport/dispatch/streaming/stream.ts +0 -8
- package/src/transport/dispatch/systemone.ts +22 -42
- package/src/transport/dispatch/token-count.ts +23 -39
- package/src/transport/dispatch/upstream.ts +1 -3
- package/src/transport/dispatch/websearch.ts +22 -41
- package/src/transport/failure-policy.ts +59 -178
- package/src/transport/guards/abuse-guard.ts +10 -0
- package/src/transport/guards/auth-guard.ts +109 -109
- package/src/transport/guards/error-lifecycle.ts +1 -1
- package/src/transport/guards/pipeline.ts +220 -220
- package/src/transport/guards/request-context.ts +245 -238
- package/src/transport/request/degradation.ts +20 -1
- package/src/transport/request/inflight.ts +4 -114
- package/src/transport/request/preparer.ts +6 -6
- package/src/transport/request/state.ts +66 -38
- package/src/transport/request/tenant-preferences.ts +1 -1
- package/src/transport/routing/route-catalog.ts +41 -31
- package/src/transport/routing/router.ts +2 -2
- package/src/transport/surface/messages/parse.ts +1 -1
- package/src/workers/buddy-growth-tasks.ts +346 -425
- package/src/workers/daily-checkin.ts +17 -6
- package/src/workers/quota-refresh-sweep.ts +2 -2
- package/src/workers/scheduler.ts +17 -1
- package/dist/dashboard/assets/BannedPage-P76QLGJA.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DdBggJS9.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-aN1iXhhP.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DE-F1nxZ.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-3CUxMy2X.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-B_0FUKWc.js +0 -1
- package/dist/dashboard/assets/LoginPage-B_0FUKWc.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-DKVslqG3.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js +0 -1
- package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js +0 -7
- package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BjldQwIU.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-DUIoLRxk.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-_bCaTM0v.js +0 -1
- package/dist/dashboard/assets/SetupPage-_bCaTM0v.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-fI7JkdIB.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-CHVZqskZ.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-DiFBymHj.js +0 -56
- package/dist/dashboard/assets/UsagePage-DiFBymHj.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-DFY_fZU4.js.gz +0 -0
- package/dist/dashboard/assets/app-BOJ1_xUB.css +0 -2
- package/dist/dashboard/assets/app-BOJ1_xUB.css.gz +0 -0
- package/dist/dashboard/assets/assert-CwdnZARt.js.gz +0 -0
- package/dist/dashboard/assets/badge-Dr-uf0Au.js.gz +0 -0
- package/dist/dashboard/assets/banner-BddmMdoS.js.gz +0 -0
- package/dist/dashboard/assets/card-CK0PXWlX.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-CcB6mGh2.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DGb2P9Yx.js.gz +0 -0
- package/dist/dashboard/assets/cog-CdWhsajs.js +0 -1
- package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
- package/dist/dashboard/assets/dialog-LkfuL50u.js.gz +0 -0
- package/dist/dashboard/assets/drawer-ZbEZ_84c.js.gz +0 -0
- package/dist/dashboard/assets/entry-BJfsVMvA.js.gz +0 -0
- package/dist/dashboard/assets/entry-CWBY5A0f.js.gz +0 -0
- package/dist/dashboard/assets/entry-CvC1-Zqx.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-BxpznFLt.js.gz +0 -0
- package/dist/dashboard/assets/index-CDzP3ovJ.js.gz +0 -0
- package/dist/dashboard/assets/inline-Ae13MQ5w.js.gz +0 -0
- package/dist/dashboard/assets/input-D23fo7cp.js.gz +0 -0
- package/dist/dashboard/assets/layout-CSsxjj5P.js.gz +0 -0
- package/dist/dashboard/assets/list-row-dmbwZMDY.js.gz +0 -0
- package/dist/dashboard/assets/live-BG1oUsZZ.js.gz +0 -0
- package/dist/dashboard/assets/mutation-DSF-e4ho.js.gz +0 -0
- package/dist/dashboard/assets/numbers-Bm_aECY7.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-CF56nYIV.js.gz +0 -0
- package/dist/dashboard/assets/routing-CeA71n7G.js.gz +0 -0
- package/dist/dashboard/assets/select-KIT0rC6G.js.gz +0 -0
- package/dist/dashboard/assets/settings-B_2gkePf.js.gz +0 -0
- package/dist/dashboard/assets/stack-D7WOSzcX.js.gz +0 -0
- package/dist/dashboard/assets/state-BBxtKZOQ.js.gz +0 -0
- package/dist/dashboard/assets/switch-DNS_D5Ea.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BgoxdoMk.js.gz +0 -0
- /package/dist/dashboard/assets/{app-CaXZ9SCn.js → app-BaWd0puM.js} +0 -0
- /package/dist/dashboard/assets/{app-CaXZ9SCn.js.gz → app-BaWd0puM.js.gz} +0 -0
|
@@ -1,425 +1,346 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The buddy growth/credit tasks: one scheduled task per upstream capability.
|
|
3
|
-
*
|
|
4
|
-
* Six tasks, each independently switchable through `CARTETHYIA_DISABLED_TASKS`,
|
|
5
|
-
* because each is a write to an account's growth state and an operator may want
|
|
6
|
-
* some of them and not others. Registration lives in `src/boot/dependencies.ts`;
|
|
7
|
-
* this module owns what a tick actually does.
|
|
8
|
-
*
|
|
9
|
-
* Two properties hold for every task here:
|
|
10
|
-
*
|
|
11
|
-
* 1. **Once per account per day**, enforced by `buddy_task_ledger` — the same
|
|
12
|
-
* day-marker pattern the daily check-in uses. Inserting a row that conflicts
|
|
13
|
-
* is exactly the "already settled today" answer, so a sweep that ticks every
|
|
14
|
-
* few minutes cannot re-POST a claim.
|
|
15
|
-
* 2. **A gamification failure never marks an account unhealthy.** No task here
|
|
16
|
-
* records a health failure: an endpoint that 404s or answers a business error
|
|
17
|
-
* is a display-level miss, and cooling an account over it would take working
|
|
18
|
-
* models out of rotation for a growth-page problem.
|
|
19
|
-
*/
|
|
20
|
-
import { and, eq } from "drizzle-orm";
|
|
21
|
-
import { randomUUID } from "node:crypto";
|
|
22
|
-
import type { CartethyiaDatabase } from "../persistence/db-handle";
|
|
23
|
-
import { buddyTaskLedger, providerAccounts } from "../persistence/schema";
|
|
24
|
-
import { log } from "../observability/logger";
|
|
25
|
-
import { sleep } from "../shared/sleep";
|
|
26
|
-
import { buddyAccountUid } from "../providers/integrations/buddy/buddy-oauth-shared";
|
|
27
|
-
import {
|
|
28
|
-
adoptFirstBuddy,
|
|
29
|
-
agreeBuddyAgreement,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
.
|
|
114
|
-
.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
credential
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
})
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Growth-activity report: the `/v2/report` batch that lights the login streak and
|
|
349
|
-
* scores the daily growth event.
|
|
350
|
-
*
|
|
351
|
-
* It is a once-a-day task with its own ledger, gated by the same
|
|
352
|
-
* (day, account, task) ledger as every other growth task: running it on the
|
|
353
|
-
* quota sweep would post two rounds of four events per account per sweep, and
|
|
354
|
-
* the sweep runs every minute.
|
|
355
|
-
*
|
|
356
|
-
* The report is not a credit claim, so its failure never marks an account
|
|
357
|
-
* unhealthy — the result is logged and the ledger is stamped either way.
|
|
358
|
-
*/
|
|
359
|
-
export function buddyActivityReportTask(deps: BuddyTaskDeps) {
|
|
360
|
-
return runTask(deps, "buddy-activity-report", async (call) => {
|
|
361
|
-
if (call.uid === undefined) {
|
|
362
|
-
return { state: "skipped", error: "No uid in this credential.", credit: null };
|
|
363
|
-
}
|
|
364
|
-
const report = await fetchBuddyActivityReport({
|
|
365
|
-
providerId: call.providerId,
|
|
366
|
-
accountId: call.accountId,
|
|
367
|
-
credential: call.credential,
|
|
368
|
-
userId: call.uid,
|
|
369
|
-
fetcher: call.fetcher,
|
|
370
|
-
});
|
|
371
|
-
if (report.state === "reported") return { state: "claimed", error: null, credit: null };
|
|
372
|
-
if (report.state === "unavailable") return { state: "already", error: null, credit: null };
|
|
373
|
-
return { state: "skipped", error: report.error, credit: null };
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// ---------------------------------------------------------------------------
|
|
378
|
-
// Usage sync: the real credit deductions, stored
|
|
379
|
-
// ---------------------------------------------------------------------------
|
|
380
|
-
|
|
381
|
-
/** Days of usage detail to read. The upstream's own window is 30. */
|
|
382
|
-
const USAGE_DAYS = 30;
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Reads and stores every buddy account's real credit deductions.
|
|
386
|
-
*
|
|
387
|
-
* Not a growth task — it is a read — but it shares the walk and the credential
|
|
388
|
-
* resolution, and it is what gives the console a usage figure the upstream
|
|
389
|
-
* actually reported.
|
|
390
|
-
*/
|
|
391
|
-
export async function syncBuddyUsage(deps: BuddyTaskDeps): Promise<number> {
|
|
392
|
-
const accounts = await (deps.listAccounts?.() ?? defaultAccounts(deps.db));
|
|
393
|
-
let stored = 0;
|
|
394
|
-
for (const account of accounts) {
|
|
395
|
-
let credential: string;
|
|
396
|
-
try {
|
|
397
|
-
credential = await deps.resolveCredential(account.providerId, account.id);
|
|
398
|
-
} catch {
|
|
399
|
-
continue;
|
|
400
|
-
}
|
|
401
|
-
if (credential.length === 0) continue;
|
|
402
|
-
const base = providerBaseUrl(account.providerId);
|
|
403
|
-
if (base === undefined || base.length === 0) continue;
|
|
404
|
-
const usage = await fetchBuddyRequestUsage({
|
|
405
|
-
// The usage-detail endpoint hangs off the provider's own origin, not the
|
|
406
|
-
// `/v2` chat prefix.
|
|
407
|
-
url: `${base.replace(/\/+$/, "").replace(/\/v2$/, "")}/billing/meter/get-user-request-usage`,
|
|
408
|
-
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
409
|
-
credential,
|
|
410
|
-
fetcher: deps.fetcher,
|
|
411
|
-
days: USAGE_DAYS,
|
|
412
|
-
...(buddyAccountUid(credential) === undefined
|
|
413
|
-
? {}
|
|
414
|
-
: { uid: buddyAccountUid(credential) }),
|
|
415
|
-
});
|
|
416
|
-
if (usage.rows.length === 0) continue;
|
|
417
|
-
stored += await saveBuddyUsageSnapshots(deps.db, account.id, usage.rows);
|
|
418
|
-
}
|
|
419
|
-
return stored;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/** A stable idempotency token, for callers that need one outside a task. */
|
|
423
|
-
export function growthClientToken(prefix: string): string {
|
|
424
|
-
return `${prefix}-${randomUUID()}`;
|
|
425
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The buddy growth/credit tasks: one scheduled task per upstream capability.
|
|
3
|
+
*
|
|
4
|
+
* Six tasks, each independently switchable through `CARTETHYIA_DISABLED_TASKS`,
|
|
5
|
+
* because each is a write to an account's growth state and an operator may want
|
|
6
|
+
* some of them and not others. Registration lives in `src/boot/dependencies.ts`;
|
|
7
|
+
* this module owns what a tick actually does.
|
|
8
|
+
*
|
|
9
|
+
* Two properties hold for every task here:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Once per account per day**, enforced by `buddy_task_ledger` — the same
|
|
12
|
+
* day-marker pattern the daily check-in uses. Inserting a row that conflicts
|
|
13
|
+
* is exactly the "already settled today" answer, so a sweep that ticks every
|
|
14
|
+
* few minutes cannot re-POST a claim.
|
|
15
|
+
* 2. **A gamification failure never marks an account unhealthy.** No task here
|
|
16
|
+
* records a health failure: an endpoint that 404s or answers a business error
|
|
17
|
+
* is a display-level miss, and cooling an account over it would take working
|
|
18
|
+
* models out of rotation for a growth-page problem.
|
|
19
|
+
*/
|
|
20
|
+
import { and, eq } from "drizzle-orm";
|
|
21
|
+
import { randomUUID } from "node:crypto";
|
|
22
|
+
import type { CartethyiaDatabase } from "../persistence/db-handle";
|
|
23
|
+
import { buddyTaskLedger, providerAccounts } from "../persistence/schema";
|
|
24
|
+
import { log } from "../observability/logger";
|
|
25
|
+
import { sleep } from "../shared/sleep";
|
|
26
|
+
import { buddyAccountUid } from "../providers/integrations/buddy/buddy-oauth-shared";
|
|
27
|
+
import {
|
|
28
|
+
adoptFirstBuddy,
|
|
29
|
+
agreeBuddyAgreement,
|
|
30
|
+
readBuddyFirstTask,
|
|
31
|
+
readBuddyInfo,
|
|
32
|
+
useBuddyMakeupCard,
|
|
33
|
+
type BuddyGrowthCall,
|
|
34
|
+
type BuddyGrowthResult,
|
|
35
|
+
} from "../providers/integrations/buddy/buddy-growth-shared";
|
|
36
|
+
import { fetchBuddyRequestUsage } from "../providers/integrations/buddy/buddy-usage-shared";
|
|
37
|
+
import { saveBuddyUsageSnapshots } from "../providers/integrations/buddy/buddy-usage-store";
|
|
38
|
+
import { fetchBuddyActivityReport } from "../providers/integrations/buddy/buddy-checkin";
|
|
39
|
+
import { withDirectFallback } from "./daily-checkin";
|
|
40
|
+
import { providerBaseUrl, BUDDY_PROVIDER_IDS } from "../providers/provider-metadata";
|
|
41
|
+
import type { FetchLike } from "../providers/quota/quota-contracts";
|
|
42
|
+
|
|
43
|
+
/** The task names an operator can switch off. */
|
|
44
|
+
export const BUDDY_TASK_NAMES = [
|
|
45
|
+
"buddy-streak",
|
|
46
|
+
"buddy-adopt",
|
|
47
|
+
"buddy-activity-report",
|
|
48
|
+
] as const;
|
|
49
|
+
export type BuddyTaskName = (typeof BUDDY_TASK_NAMES)[number];
|
|
50
|
+
|
|
51
|
+
/** One account a growth task can run for. */
|
|
52
|
+
export interface BuddyTaskAccount {
|
|
53
|
+
readonly id: string;
|
|
54
|
+
readonly providerId: string;
|
|
55
|
+
readonly label?: string | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface BuddyTaskDeps {
|
|
59
|
+
readonly db: CartethyiaDatabase;
|
|
60
|
+
/** Resolves the stored credential through the refresh-aware path. */
|
|
61
|
+
readonly resolveCredential: (providerId: string, accountId: string) => Promise<string>;
|
|
62
|
+
readonly fetcher: FetchLike;
|
|
63
|
+
/** Accounts to walk; defaults to every buddy account with a credential. */
|
|
64
|
+
readonly listAccounts?: () => Promise<readonly BuddyTaskAccount[]>;
|
|
65
|
+
/** Injected clock so a test can advance the day. */
|
|
66
|
+
readonly now?: () => Date;
|
|
67
|
+
/** Pause between accounts, so a pass does not look like a burst. */
|
|
68
|
+
readonly interAccountDelayMs?: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Providers whose growth facade is worth calling. */
|
|
72
|
+
function isBuddy(providerId: string): boolean {
|
|
73
|
+
return BUDDY_PROVIDER_IDS.has(providerId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The upstream's own day boundary: growth days roll at 00:00 CST. */
|
|
77
|
+
export function buddyDayKey(now: Date = new Date()): string {
|
|
78
|
+
return new Date(now.getTime() + 8 * 3600_000).toISOString().slice(0, 10);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Marks one (day, account, task) settled.
|
|
83
|
+
*
|
|
84
|
+
* Only ever writes a slot, never releases one — the caller is asserting "this
|
|
85
|
+
* account is done with this task today", which is exactly what the row means.
|
|
86
|
+
* A conflict is the ordinary case after a restart and is not an error.
|
|
87
|
+
*/
|
|
88
|
+
async function markClaimed(
|
|
89
|
+
db: CartethyiaDatabase,
|
|
90
|
+
dayKey: string,
|
|
91
|
+
accountId: string,
|
|
92
|
+
task: string,
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
try {
|
|
95
|
+
await db
|
|
96
|
+
.insert(buddyTaskLedger)
|
|
97
|
+
.values({ dayKey, accountId, task })
|
|
98
|
+
.onConflictDoNothing();
|
|
99
|
+
} catch (error) {
|
|
100
|
+
log.warn(`[${task}] ledger stamp failed for account=${accountId}`, error as Error);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** True when this (day, account, task) already has a marker. */
|
|
105
|
+
async function claimedToday(
|
|
106
|
+
db: CartethyiaDatabase,
|
|
107
|
+
dayKey: string,
|
|
108
|
+
accountId: string,
|
|
109
|
+
task: string,
|
|
110
|
+
): Promise<boolean> {
|
|
111
|
+
try {
|
|
112
|
+
const rows = await db
|
|
113
|
+
.select({ dayKey: buddyTaskLedger.dayKey })
|
|
114
|
+
.from(buddyTaskLedger)
|
|
115
|
+
.where(
|
|
116
|
+
and(
|
|
117
|
+
eq(buddyTaskLedger.dayKey, dayKey),
|
|
118
|
+
eq(buddyTaskLedger.accountId, accountId),
|
|
119
|
+
eq(buddyTaskLedger.task, task),
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
.limit(1);
|
|
123
|
+
return rows.length > 0;
|
|
124
|
+
} catch {
|
|
125
|
+
// Unreadable means unknown, never "not claimed": the safe answer is to try,
|
|
126
|
+
// and the upstream's own idempotency is the backstop.
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Every credential-bearing buddy account. */
|
|
131
|
+
async function defaultAccounts(db: CartethyiaDatabase): Promise<readonly BuddyTaskAccount[]> {
|
|
132
|
+
const rows = await db
|
|
133
|
+
.select({
|
|
134
|
+
id: providerAccounts.id,
|
|
135
|
+
providerId: providerAccounts.providerId,
|
|
136
|
+
label: providerAccounts.label,
|
|
137
|
+
})
|
|
138
|
+
.from(providerAccounts)
|
|
139
|
+
.where(eq(providerAccounts.status, "active"));
|
|
140
|
+
return rows.filter((row) => isBuddy(row.providerId));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Reads the account uid from a JWT, memoised per credential.
|
|
144
|
+
*
|
|
145
|
+
* A growth pass walks many accounts, and the uid is only ever present in a
|
|
146
|
+
* JWT; an opaque API key has none. Decoding the token once per account is
|
|
147
|
+
* cheap, but the same credential can appear in several tasks in one pass, so
|
|
148
|
+
* the decode is cached per pass instead of per call.
|
|
149
|
+
*/
|
|
150
|
+
function resolveUid(
|
|
151
|
+
credential: string,
|
|
152
|
+
cache: Map<string, string | undefined>,
|
|
153
|
+
): string | undefined {
|
|
154
|
+
const cached = cache.get(credential);
|
|
155
|
+
if (cached !== undefined) return cached;
|
|
156
|
+
const uid = buddyAccountUid(credential);
|
|
157
|
+
cache.set(credential, uid);
|
|
158
|
+
return uid;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** One account's task run, with the credential resolved and the call wired. */
|
|
162
|
+
async function forAccount(
|
|
163
|
+
uidCache: Map<string, string | undefined>,
|
|
164
|
+
deps: BuddyTaskDeps,
|
|
165
|
+
account: BuddyTaskAccount,
|
|
166
|
+
task: BuddyTaskName,
|
|
167
|
+
run: (call: BuddyGrowthCall) => Promise<BuddyGrowthResult | null>,
|
|
168
|
+
): Promise<BuddyGrowthResult | null> {
|
|
169
|
+
let credential: string;
|
|
170
|
+
try {
|
|
171
|
+
credential = await deps.resolveCredential(account.providerId, account.id);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
log.warn(
|
|
174
|
+
`[${task}] account=${account.id} credential unresolved:`,
|
|
175
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
176
|
+
);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
if (credential.length === 0) return null;
|
|
180
|
+
const call: BuddyGrowthCall = {
|
|
181
|
+
baseUrl: providerBaseUrl(account.providerId) ?? "",
|
|
182
|
+
headers: { "X-Product": "SaaS", "Content-Type": "application/json", Accept: "application/json" },
|
|
183
|
+
accountId: account.id,
|
|
184
|
+
providerId: account.providerId,
|
|
185
|
+
credential,
|
|
186
|
+
fetcher: withDirectFallback(deps.fetcher) as FetchLike,
|
|
187
|
+
...(resolveUid(credential, uidCache) === undefined
|
|
188
|
+
? {}
|
|
189
|
+
: { uid: resolveUid(credential, uidCache) }),
|
|
190
|
+
};
|
|
191
|
+
if (call.baseUrl.length === 0) return null;
|
|
192
|
+
return run(call);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Walks the accounts, skipping any already settled today. */
|
|
196
|
+
async function runTask(
|
|
197
|
+
deps: BuddyTaskDeps,
|
|
198
|
+
task: BuddyTaskName,
|
|
199
|
+
run: (call: BuddyGrowthCall) => Promise<BuddyGrowthResult | null>,
|
|
200
|
+
): Promise<{ readonly attempted: number; readonly claimed: number }> {
|
|
201
|
+
const dayKey = buddyDayKey(deps.now?.() ?? new Date());
|
|
202
|
+
const uidCache = new Map<string, string | undefined>();
|
|
203
|
+
const accounts = await (deps.listAccounts?.() ?? defaultAccounts(deps.db));
|
|
204
|
+
let attempted = 0;
|
|
205
|
+
let claimed = 0;
|
|
206
|
+
const delay = deps.interAccountDelayMs ?? 0;
|
|
207
|
+
for (const [index, account] of accounts.entries()) {
|
|
208
|
+
if (index > 0 && delay > 0) await sleep(delay);
|
|
209
|
+
if (await claimedToday(deps.db, dayKey, account.id, task)) continue;
|
|
210
|
+
attempted += 1;
|
|
211
|
+
let result: BuddyGrowthResult | null = null;
|
|
212
|
+
try {
|
|
213
|
+
result = await forAccount(uidCache, deps, account, task, run);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
log.warn(
|
|
216
|
+
`[${task}] account=${account.id} failed:`,
|
|
217
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (result !== null) {
|
|
221
|
+
log.info(
|
|
222
|
+
`[${task}] account=${account.id} provider=${account.providerId} state=${result.state}` +
|
|
223
|
+
`${result.credit !== null ? ` credit=${result.credit}` : ""}` +
|
|
224
|
+
`${result.error !== null ? ` detail=${result.error}` : ""}`,
|
|
225
|
+
);
|
|
226
|
+
if (result.state === "claimed" || result.state === "already") claimed += 1;
|
|
227
|
+
}
|
|
228
|
+
// Marked whether it landed or not: a repeat write the same day can only
|
|
229
|
+
// produce the duplicate answer, and not marking would re-POST it forever.
|
|
230
|
+
await markClaimed(deps.db, dayKey, account.id, task);
|
|
231
|
+
}
|
|
232
|
+
return { attempted, claimed };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// The six tasks
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Streak makeup: spend a card on yesterday's miss so the streak does not break.
|
|
241
|
+
*
|
|
242
|
+
* Everything other than an actual spend is `skipped`, so an account with no
|
|
243
|
+
* miss, no card, or no heatmap produces no log noise beyond one info line.
|
|
244
|
+
*/
|
|
245
|
+
export function buddyStreakTask(deps: BuddyTaskDeps) {
|
|
246
|
+
return runTask(deps, "buddy-streak", (call) => useBuddyMakeupCard(call));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* First-buddy adoption: accept the agreement, then adopt.
|
|
251
|
+
*
|
|
252
|
+
* Gated on the task actually being offered and unlocked — adopting blind writes
|
|
253
|
+
* to an account's growth state on the strength of an endpoint existing.
|
|
254
|
+
*/
|
|
255
|
+
export function buddyAdoptTask(deps: BuddyTaskDeps) {
|
|
256
|
+
return runTask(deps, "buddy-adopt", async (call) => {
|
|
257
|
+
const info = await readBuddyInfo(call);
|
|
258
|
+
if (info === null || info.hasBuddy) return null;
|
|
259
|
+
const task = await readBuddyFirstTask(call);
|
|
260
|
+
if (task === null) return null;
|
|
261
|
+
const agreed = await agreeBuddyAgreement(call);
|
|
262
|
+
// Only a hard error stops the sequence; a duplicate agreement is fine.
|
|
263
|
+
if (agreed.state === "error") return agreed;
|
|
264
|
+
return adoptFirstBuddy(call);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Growth-activity report: the `/v2/report` batch that lights the login streak and
|
|
270
|
+
* scores the daily growth event.
|
|
271
|
+
*
|
|
272
|
+
* It is a once-a-day task with its own ledger, gated by the same
|
|
273
|
+
* (day, account, task) ledger as every other growth task: running it on the
|
|
274
|
+
* quota sweep would post two rounds of four events per account per sweep, and
|
|
275
|
+
* the sweep runs every minute.
|
|
276
|
+
*
|
|
277
|
+
* The report is not a credit claim, so its failure never marks an account
|
|
278
|
+
* unhealthy — the result is logged and the ledger is stamped either way.
|
|
279
|
+
*/
|
|
280
|
+
export function buddyActivityReportTask(deps: BuddyTaskDeps) {
|
|
281
|
+
return runTask(deps, "buddy-activity-report", async (call) => {
|
|
282
|
+
if (call.uid === undefined) {
|
|
283
|
+
return { state: "skipped", error: "No uid in this credential.", credit: null };
|
|
284
|
+
}
|
|
285
|
+
const report = await fetchBuddyActivityReport({
|
|
286
|
+
providerId: call.providerId,
|
|
287
|
+
accountId: call.accountId,
|
|
288
|
+
credential: call.credential,
|
|
289
|
+
userId: call.uid,
|
|
290
|
+
fetcher: call.fetcher,
|
|
291
|
+
});
|
|
292
|
+
if (report.state === "reported") return { state: "claimed", error: null, credit: null };
|
|
293
|
+
if (report.state === "unavailable") return { state: "already", error: null, credit: null };
|
|
294
|
+
return { state: "skipped", error: report.error, credit: null };
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// Usage sync: the real credit deductions, stored
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
/** Days of usage detail to read. The upstream's own window is 30. */
|
|
303
|
+
const USAGE_DAYS = 30;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Reads and stores every buddy account's real credit deductions.
|
|
307
|
+
*
|
|
308
|
+
* Not a growth task — it is a read — but it shares the walk and the credential
|
|
309
|
+
* resolution, and it is what gives the console a usage figure the upstream
|
|
310
|
+
* actually reported.
|
|
311
|
+
*/
|
|
312
|
+
export async function syncBuddyUsage(deps: BuddyTaskDeps): Promise<number> {
|
|
313
|
+
const accounts = await (deps.listAccounts?.() ?? defaultAccounts(deps.db));
|
|
314
|
+
let stored = 0;
|
|
315
|
+
for (const account of accounts) {
|
|
316
|
+
let credential: string;
|
|
317
|
+
try {
|
|
318
|
+
credential = await deps.resolveCredential(account.providerId, account.id);
|
|
319
|
+
} catch {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (credential.length === 0) continue;
|
|
323
|
+
const base = providerBaseUrl(account.providerId);
|
|
324
|
+
if (base === undefined || base.length === 0) continue;
|
|
325
|
+
const usage = await fetchBuddyRequestUsage({
|
|
326
|
+
// The usage-detail endpoint hangs off the provider's own origin, not the
|
|
327
|
+
// `/v2` chat prefix.
|
|
328
|
+
url: `${base.replace(/\/+$/, "").replace(/\/v2$/, "")}/billing/meter/get-user-request-usage`,
|
|
329
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
330
|
+
credential,
|
|
331
|
+
fetcher: deps.fetcher,
|
|
332
|
+
days: USAGE_DAYS,
|
|
333
|
+
...(buddyAccountUid(credential) === undefined
|
|
334
|
+
? {}
|
|
335
|
+
: { uid: buddyAccountUid(credential) }),
|
|
336
|
+
});
|
|
337
|
+
if (usage.rows.length === 0) continue;
|
|
338
|
+
stored += await saveBuddyUsageSnapshots(deps.db, account.id, usage.rows);
|
|
339
|
+
}
|
|
340
|
+
return stored;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** A stable idempotency token, for callers that need one outside a task. */
|
|
344
|
+
export function growthClientToken(prefix: string): string {
|
|
345
|
+
return `${prefix}-${randomUUID()}`;
|
|
346
|
+
}
|