cartethyia 2.7.8 → 2.8.0
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 +3 -3
- package/README.md +29 -59
- package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js +1 -0
- package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-BRWKPQPf.js → CliToolDetailPage-Bqj2aa1T.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BP-Y5iHN.js → CliToolsPage-CXsaOAO2.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js +1 -0
- package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
- package/dist/dashboard/assets/{InjectSystemPromptPanel-Cv-2Ol_K.js → InjectSystemPromptPanel-BWFLECmz.js} +2 -2
- package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-bKfz_b1K.js → LoginPage-CgoPc5B9.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-B-WQxx_H.js → ModelPicker-D9bFQern.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js +1 -0
- package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +8 -0
- package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-emlTuxz6.js → ProxyPage-BwNC_-Cx.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js +1 -0
- package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CctTga2C.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-CUZrKs9r.js → SetupPage-febLU4r1.js} +1 -1
- package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-jWmUdDpy.js → StudioPage-M6fvR5Wz.js} +1 -1
- package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-nyeuRqz9.js +56 -0
- package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BFOlvrJC.js +1 -0
- package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
- package/dist/dashboard/assets/app-Dx2QgsNj.css +2 -0
- package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
- package/dist/dashboard/assets/{assert-CidQVoNo.js → assert-CqTLw5e7.js} +1 -1
- package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DyI_mJfi.js → badge-C83EqLB0.js} +1 -1
- package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
- package/dist/dashboard/assets/ban-durations-B58FyRxC.js +1 -0
- package/dist/dashboard/assets/ban-durations-B58FyRxC.js.gz +0 -0
- package/dist/dashboard/assets/{banner-B-yXtCqb.js → banner-D1xn7pIe.js} +1 -1
- package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
- package/dist/dashboard/assets/brand/bansos_page.webp +0 -0
- package/dist/dashboard/assets/brand/denia_bansos.webp +0 -0
- package/dist/dashboard/assets/brand/favicon.webp +0 -0
- package/dist/dashboard/assets/brand/login_register.webp +0 -0
- package/dist/dashboard/assets/brand/login_register_initial.webp +0 -0
- package/dist/dashboard/assets/brand/og_bansos.webp +0 -0
- package/dist/dashboard/assets/brand/og_image.webp +0 -0
- package/dist/dashboard/assets/brand/sidebar_ico.webp +0 -0
- package/dist/dashboard/assets/{card-_HwAEqFF.js → card-DU3Lq5Lm.js} +1 -1
- package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-Dw1U_drK.js +1 -0
- package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-ByjCk0o9.js → clipboard-button-CEL5Sg_J.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-BZhOlfz-.js → dialog-BjFR5UHy.js} +1 -1
- package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-BADqIRYP.js → drawer-HvBW0AjI.js} +1 -1
- package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
- package/dist/dashboard/assets/entry-BSu3gFlt.js +4 -0
- package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
- package/dist/dashboard/assets/{entry-COaC-irN.js → entry-CECVELyy.js} +1 -1
- package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
- package/dist/dashboard/assets/entry-DYYofQ7W.js +1 -0
- package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-DcHJyRiu.js → formatters-DLW4i8zK.js} +1 -1
- package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
- package/dist/dashboard/assets/{index-CgKmFngv.js → index-1-fCqe64.js} +2 -2
- package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
- package/dist/dashboard/assets/{inline-B2BirKry.js → inline-DGvU2oLK.js} +1 -1
- package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
- package/dist/dashboard/assets/{input-D-qV9dKY.js → input-iNnO1X2K.js} +1 -1
- package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
- package/dist/dashboard/assets/{layout-B7tVWhtV.js → layout-0q8UGypb.js} +1 -1
- package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-CtkGE3bF.js → list-row-C0w2bQjJ.js} +1 -1
- package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
- package/dist/dashboard/assets/{live-DGizF06-.js → live-M5aJ_xCa.js} +1 -1
- package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-GU7XdEjz.js → mutation-BQ0ccH_A.js} +1 -1
- package/dist/dashboard/assets/mutation-BQ0ccH_A.js.gz +0 -0
- package/dist/dashboard/assets/{network-C9UE-bOr.js → network-BKeQGT3K.js} +1 -1
- package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BnAd22a_.js → numbers-CivAk3da.js} +1 -1
- package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
- package/dist/dashboard/assets/{pagination-BAywyMzS.js → pagination-CpzvI5ep.js} +1 -1
- package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
- package/dist/dashboard/assets/providers/devin.svg +9 -0
- package/dist/dashboard/assets/providers/devin.svg.gz +0 -0
- package/dist/dashboard/assets/providers/windsurf.svg +10 -0
- package/dist/dashboard/assets/providers/windsurf.svg.gz +0 -0
- package/dist/dashboard/assets/providers/zro.svg +3 -0
- package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
- package/dist/dashboard/assets/{query-keys-YSoQOUh6.js → query-keys-IKLh0YFd.js} +1 -1
- package/dist/dashboard/assets/query-keys-IKLh0YFd.js.gz +0 -0
- package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
- package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
- package/dist/dashboard/assets/routing-BFQX2VBc.js +1 -0
- package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
- package/dist/dashboard/assets/{select-BCOcv8WK.js → select-CXbck0jG.js} +2 -2
- package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
- package/dist/dashboard/assets/settings-C_XvMHh9.js +1 -0
- package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
- package/dist/dashboard/assets/{stack-ByKt3Q1g.js → stack-DggRiCxE.js} +1 -1
- package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
- package/dist/dashboard/assets/{state-DXbGTx7N.js → state-ClFzfbYp.js} +1 -1
- package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
- package/dist/dashboard/assets/story/25817331.webp +0 -0
- package/dist/dashboard/assets/story/Shorekeeper.webp +0 -0
- package/dist/dashboard/assets/story/cartethyia-god.webp +0 -0
- package/dist/dashboard/assets/story/fleurdelys_plus.webp +0 -0
- package/dist/dashboard/assets/story/jinhsi-blossom.webp +0 -0
- package/dist/dashboard/assets/story/phrolova-a.webp +0 -0
- package/dist/dashboard/assets/story/requestdeniawokkjpg.webp +0 -0
- package/dist/dashboard/assets/story/wuthering-waves-hiyuki-aemeath-hiyuki-rover.webp +0 -0
- package/dist/dashboard/assets/{switch-ZeTsW623.js → switch-CTIFxKqS.js} +1 -1
- package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-CbCP4w1o.js → useMutation-BClG3yWj.js} +1 -1
- package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
- package/dist/dashboard/console-backgrounds.webp +0 -0
- package/dist/dashboard/console-backgrounds_initial.webp +0 -0
- package/dist/dashboard/donation/qris-risun-marke.webp +0 -0
- package/dist/dashboard/index.html +3 -3
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +59 -56
- package/migrations/full-postgres/0044_add_models_warning_metadata.sql +10 -0
- package/migrations/full-postgres/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
- package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
- package/migrations/lite-sqlite3/0044_add_models_warning_metadata.sql +11 -0
- package/migrations/lite-sqlite3/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
- package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
- package/package.json +1 -1
- package/src/boot/app.ts +10 -13
- package/src/boot/dependencies.ts +12 -4
- package/src/boot/flush.ts +20 -0
- package/src/boot/lifecycle.ts +7 -5
- package/src/config/cooldowns.ts +1 -1
- package/src/config/routing.ts +26 -0
- package/src/config/spec-metadata.ts +72 -0
- package/src/config/spec.ts +20 -6
- package/src/console/api-keys/contracts.ts +12 -1
- package/src/console/api-keys/routes.ts +50 -31
- package/src/console/auth/account-reset.ts +7 -9
- package/src/console/backup/contracts.ts +4 -0
- package/src/console/backup/export-endpoint.ts +7 -5
- package/src/console/cli-tools/service.ts +6 -12
- package/src/console/console-router-guards.ts +23 -16
- package/src/console/console-router.ts +7 -0
- package/src/console/dashboard-assets.ts +5 -5
- package/src/console/domain-registration.ts +27 -2
- package/src/console/inferhub/normalize.ts +24 -28
- package/src/console/inferhub/types.ts +3 -0
- package/src/console/ip-bans/routes.ts +93 -0
- package/src/console/observability/store.ts +0 -53
- package/src/console/providers/catalog/catalog-projections.ts +5 -2
- package/src/console/providers/catalog/contracts.ts +123 -0
- package/src/console/providers/catalog/discover-constants.ts +6 -0
- package/src/console/providers/catalog/model-operations.ts +122 -10
- package/src/console/providers/catalog/provider-operations.ts +116 -8
- package/src/console/providers/catalog/public-model-store.ts +0 -0
- package/src/console/providers/catalog/routes.ts +65 -4
- package/src/console/providers/catalog/store-helpers.ts +30 -1
- package/src/console/providers/catalog/store.ts +249 -4
- package/src/console/quota/account-quota-global.ts +97 -19
- package/src/console/quota/account-quota-shared.ts +49 -0
- package/src/console/quota/account-quota-tenant.ts +6 -18
- package/src/console/quota/native-identity.ts +2 -0
- package/src/console/quota/quota-view.ts +19 -3
- package/src/console/routing/pools/operations.ts +31 -46
- package/src/console/settings/contracts.ts +18 -0
- package/src/console/settings/store.ts +32 -0
- package/src/console/shared/errors.ts +33 -6
- package/src/console/system-prompt/contracts.ts +0 -1
- package/src/main.ts +3 -3
- package/src/network/pool/byte-accounting.ts +3 -2
- package/src/network/pool/selector.ts +182 -39
- package/src/network/pool-cooldown-store.ts +89 -14
- package/src/network/pool-health.ts +49 -61
- package/src/network/rate-limit-verdict.ts +1 -1
- package/src/network/ssrf.ts +6 -6
- package/src/observability/event-log.ts +7 -361
- package/src/observability/event-query.ts +18 -4
- package/src/observability/performance-metrics.ts +9 -0
- package/src/observability/runtime-metrics.ts +14 -9
- package/src/observability/telemetry-buffer.ts +15 -25
- package/src/observability/telemetry-reconciliation.ts +9 -37
- package/src/observability/telemetry-row.ts +125 -0
- package/src/persistence/db-mode.ts +4 -1
- package/src/persistence/dialect.ts +30 -0
- package/src/persistence/migrate.ts +6 -3
- package/src/persistence/schema.ts +28 -2
- package/src/persistence/ttl-cache.ts +20 -2
- package/src/protocol/codecs.ts +1 -1
- package/src/protocol/dsml.ts +35 -87
- package/src/protocol/gateway-error.ts +1 -0
- package/src/protocol/markup-stream-parser.ts +195 -0
- package/src/protocol/reasoning-text.ts +19 -0
- package/src/protocol/reasoning.ts +1 -1
- package/src/protocol/request/chat.ts +14 -14
- package/src/protocol/request/gemini.ts +8 -2
- package/src/protocol/request/messages.ts +4 -5
- package/src/protocol/response/chat.ts +12 -20
- package/src/protocol/response/gemini.ts +6 -1
- package/src/protocol/response/omp-tool-call-markup.ts +43 -86
- package/src/protocol/response/responses.ts +18 -2
- package/src/protocol/sse-decoder.ts +25 -8
- package/src/protocol/stop-reason.ts +23 -1
- package/src/protocol/stream-error-frames.ts +13 -3
- package/src/protocol/think-split.ts +112 -0
- package/src/protocol/translation/dialect/qwen.ts +26 -58
- package/src/protocol/translation/quirks.ts +1 -1
- package/src/protocol/translation/tool-repair.ts +2 -2
- package/src/protocol/upstream/send-claude-messages.ts +15 -4
- package/src/protocol/upstream-parsing.ts +1 -1
- package/src/providers/adapters/compatible-adapter.ts +78 -17
- package/src/providers/authentication/oauth-client.ts +1 -1
- package/src/providers/authentication/oauth-device-flow.ts +98 -8
- package/src/providers/authentication/oauth-refresh-service.ts +153 -59
- package/src/providers/configured-provider.ts +148 -0
- package/src/providers/default-registry.ts +117 -27
- package/src/providers/discovery/discovery-types.ts +12 -0
- package/src/providers/discovery/probe-phases.ts +74 -33
- package/src/providers/discovery/probing-service.ts +293 -165
- package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
- package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
- package/src/providers/integrations/antigravity/antigravity-protocol.ts +121 -2
- package/src/providers/integrations/antigravity/antigravity.ts +13 -2
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +72 -34
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +191 -35
- package/src/providers/integrations/buddy/buddy-checkin.ts +17 -39
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +2 -2
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +5 -5
- package/src/providers/integrations/buddy/buddy-identity.ts +89 -6
- package/src/providers/integrations/buddy/buddy-media.ts +4 -5
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +2 -2
- package/src/providers/integrations/buddy/buddy-quota-shared.ts +1 -1
- package/src/providers/integrations/buddy/buddy-usage-shared.ts +1 -3
- package/src/providers/integrations/buddy/capabilities.ts +6 -43
- package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +2 -1
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +23 -1
- package/src/providers/integrations/buddy/codebuddy/provider.ts +6 -2
- package/src/providers/integrations/buddy/codebuddy/shared.ts +38 -27
- package/src/providers/integrations/claude/claude-quota.ts +14 -17
- package/src/providers/integrations/cline/cline.ts +2 -2
- package/src/providers/integrations/commandcode/quota.ts +22 -28
- package/src/providers/integrations/cursor/cursor-auth.ts +0 -8
- package/src/providers/integrations/cursor/cursor-catalog.ts +2 -0
- package/src/providers/integrations/cursor/cursor-exec.ts +745 -0
- package/src/providers/integrations/cursor/cursor-http2.ts +2 -0
- package/src/providers/integrations/cursor/cursor-request.ts +35 -20
- package/src/providers/integrations/cursor/cursor-shared.ts +26 -5
- package/src/providers/integrations/cursor/cursor.ts +139 -70
- package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
- package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
- package/src/providers/integrations/cursor/protocol/tool-canonical.ts +26 -100
- package/src/providers/integrations/devin/devin-auth.ts +353 -0
- package/src/providers/integrations/devin/devin-catalog.ts +207 -0
- package/src/providers/integrations/devin/devin-http.ts +91 -0
- package/src/providers/integrations/devin/devin-quota.ts +214 -0
- package/src/providers/integrations/devin/devin-shared.ts +111 -0
- package/src/providers/integrations/devin/devin.ts +539 -0
- package/src/providers/integrations/devin/protocol/devin-proto.ts +558 -0
- package/src/providers/integrations/devin/protocol/framing.ts +124 -0
- package/src/providers/integrations/factory/factory-oauth.ts +17 -44
- package/src/providers/integrations/factory/factory.ts +2 -2
- package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
- package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
- package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
- package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
- package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
- package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
- package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
- package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
- package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
- package/src/providers/integrations/freebuff/freebuff.ts +682 -0
- package/src/providers/integrations/freepi/freepi-catalog.ts +60 -0
- package/src/providers/integrations/freepi/freepi-discovery.ts +82 -0
- package/src/providers/integrations/freepi/freepi-headers.ts +21 -0
- package/src/providers/integrations/freepi/freepi-oauth.ts +142 -0
- package/src/providers/integrations/freepi/freepi-protocol.ts +242 -0
- package/src/providers/integrations/freepi/freepi-quota.ts +126 -0
- package/src/providers/integrations/freepi/freepi-realm.ts +35 -0
- package/src/providers/integrations/freepi/freepi.ts +292 -0
- package/src/providers/integrations/gemini.ts +2 -2
- package/src/providers/integrations/grok/grok-oauth.ts +48 -45
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
- package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
- package/src/providers/integrations/hermes/hermes-oauth.ts +94 -0
- package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +13 -21
- package/src/providers/integrations/hermes/index.ts +2 -0
- package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
- package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
- package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
- package/src/providers/integrations/kiro/kiro.ts +15 -19
- package/src/providers/integrations/ollamacloud/index.ts +2 -0
- package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
- package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
- package/src/providers/integrations/opencode/fingerprint.ts +1 -1
- package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
- package/src/providers/integrations/perch/perch-catalog.ts +105 -0
- package/src/providers/integrations/perch/perch-credential.ts +25 -0
- package/src/providers/integrations/perch/perch-oauth.ts +170 -0
- package/src/providers/integrations/perch/perch-protocol.ts +208 -0
- package/src/providers/integrations/perch/perch-quota.ts +123 -0
- package/src/providers/integrations/perch/perch-realm.ts +82 -0
- package/src/providers/integrations/perch/perch-system-prompt.ts +13 -0
- package/src/providers/integrations/perch/perch.ts +305 -0
- package/src/providers/integrations/perch/saffron-system-prompt.txt +8 -0
- package/src/providers/integrations/qoder/chat-proto.ts +328 -328
- package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
- package/src/providers/integrations/qoder/qoder-adapter.ts +90 -40
- package/src/providers/integrations/qoder/qoder-auth.ts +101 -29
- package/src/providers/integrations/qoder/qoder-cache.ts +32 -0
- package/src/providers/integrations/qoder/qoder-catalog.ts +26 -6
- package/src/providers/integrations/qoder/qoder-chat.ts +7 -21
- package/src/providers/integrations/qoder/qoder-checkin.ts +6 -6
- package/src/providers/integrations/qoder/qoder-grpc.ts +210 -7
- package/src/providers/integrations/qoder/qoder-ide.ts +5 -108
- package/src/providers/integrations/qoder/qoder-identity-cache.ts +184 -0
- package/src/providers/integrations/qoder/qoder-native-identity-store.ts +83 -18
- package/src/providers/integrations/qoder/qoder-realm.ts +9 -6
- package/src/providers/integrations/qoder/qoder-umid.ts +109 -342
- package/src/providers/integrations/qoder/qoder-virtual.ts +33 -1
- package/src/providers/integrations/qoder/qoder.ts +3 -7
- package/src/providers/integrations/trae/trae-catalog.ts +9 -14
- package/src/providers/integrations/trae/trae-headers.ts +36 -11
- package/src/providers/integrations/trae/trae-oauth.ts +14 -17
- package/src/providers/integrations/trae/trae-protocol.ts +12 -11
- package/src/providers/integrations/trae/trae-quota.ts +96 -36
- package/src/providers/integrations/trae/trae-realm.ts +74 -15
- package/src/providers/integrations/trae/trae.ts +99 -26
- package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
- package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
- package/src/providers/integrations/zro/zro.ts +204 -0
- package/src/providers/operations/account-error-classification.ts +235 -278
- package/src/providers/operations/account-health-service.ts +273 -26
- package/src/providers/operations/account-refresh-status.ts +269 -0
- package/src/providers/operations/account-tier.ts +66 -0
- package/src/providers/operations/client-version-resolver.ts +1 -1
- package/src/providers/operations/client-versions.ts +42 -150
- package/src/providers/operations/cooldown-rules.ts +56 -0
- package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
- package/src/providers/operations/provider-catalog-seeder.ts +4 -0
- package/src/providers/operations/provider-catalog-service.ts +1 -1
- package/src/providers/operations/provider-credential-service.ts +102 -32
- package/src/providers/operations/provider-rules/github.ts +48 -0
- package/src/providers/operations/provider-rules/grok.ts +33 -0
- package/src/providers/operations/provider-rules/inception.ts +49 -0
- package/src/providers/operations/provider-rules/index.ts +32 -0
- package/src/providers/operations/provider-rules/perch.ts +37 -0
- package/src/providers/operations/provider-rules/qoder.ts +70 -0
- package/src/providers/operations/provider-rules/trae.ts +81 -0
- package/src/providers/provider-contracts.ts +33 -2
- package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
- package/src/providers/provider-metadata.ts +99 -14
- package/src/providers/provider-model-definition.ts +36 -0
- package/src/providers/provider-probe.ts +15 -1
- package/src/providers/provider-registry.ts +42 -33
- package/src/providers/quota/quota-limit-windows.ts +16 -5
- package/src/providers/quota/quota-support.ts +1 -1
- package/src/providers/quota/quota-window-parser.ts +90 -18
- package/src/providers/quota/refresh.ts +96 -18
- package/src/security/abuse.ts +71 -0
- package/src/security/api-key-auth-cache.ts +1 -1
- package/src/security/api-key-auth.ts +6 -2
- package/src/transport/dispatch/attempt-finalize.ts +14 -13
- package/src/transport/dispatch/attempt-loop.ts +71 -17
- package/src/transport/dispatch/generation.ts +2 -8
- package/src/transport/dispatch/native-shared.ts +25 -0
- package/src/transport/dispatch/proxy-request.ts +9 -46
- package/src/transport/dispatch/responses-compact.ts +2 -8
- package/src/transport/dispatch/streaming/finalize.ts +9 -6
- package/src/transport/dispatch/streaming/stream.ts +9 -36
- package/src/transport/dispatch/systemone.ts +2 -8
- package/src/transport/dispatch/token-count.ts +2 -6
- package/src/transport/dispatch/upstream.ts +67 -1
- package/src/transport/drain-endpoint.ts +31 -4
- package/src/transport/failure-policy.ts +129 -37
- package/src/transport/guards/body-policy.ts +7 -1
- package/src/transport/guards/error-lifecycle.ts +13 -1
- package/src/transport/guards/request-context.ts +22 -14
- package/src/transport/request/preparer.ts +81 -71
- package/src/transport/routing/bounded-lru.ts +14 -0
- package/src/transport/routing/drain-reserve.ts +129 -0
- package/src/transport/routing/plan-gate.ts +7 -1
- package/src/transport/routing/round-robin.ts +60 -0
- package/src/transport/routing/route-catalog.ts +123 -111
- package/src/transport/routing/route-model.ts +51 -9
- package/src/transport/routing/router.ts +418 -33
- package/src/transport/surface/adapters.ts +11 -2
- package/src/transport/surface/chat/adapter.ts +2 -2
- package/src/transport/surface/chat/encode.ts +47 -4
- package/src/transport/surface/chat/parse.ts +2 -12
- package/src/transport/surface/completion.ts +44 -33
- package/src/transport/surface/content-parts.ts +21 -0
- package/src/transport/surface/messages/adapter.ts +9 -2
- package/src/transport/surface/messages/encode.ts +11 -3
- package/src/transport/surface/messages/parse.ts +2 -1
- package/src/transport/surface/messages/stream.ts +33 -17
- package/src/transport/surface/responses/encode.ts +34 -6
- package/src/transport/surface/responses/parse.ts +3 -11
- package/src/workers/daily-checkin.ts +1 -1
- package/src/workers/quota-refresh-sweep.ts +1 -1
- package/src/workers/scheduler.ts +22 -4
- package/dist/dashboard/assets/BannedPage-DeIbIiBU.js +0 -1
- package/dist/dashboard/assets/BannedPage-DeIbIiBU.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-BRWKPQPf.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-BP-Y5iHN.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-B7JHozVa.js +0 -1
- package/dist/dashboard/assets/CombosPage-B7JHozVa.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-Cv-2Ol_K.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-bKfz_b1K.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-B-WQxx_H.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js +0 -1
- package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js +0 -7
- package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-emlTuxz6.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js +0 -1
- package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js +0 -1
- package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-CUZrKs9r.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-jWmUdDpy.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-5F9tGVGG.js +0 -56
- package/dist/dashboard/assets/UsagePage-5F9tGVGG.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-KvX83mRe.js +0 -1
- package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
- package/dist/dashboard/assets/app-BEsU8hKs.css +0 -2
- package/dist/dashboard/assets/app-BEsU8hKs.css.gz +0 -0
- package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
- package/dist/dashboard/assets/badge-DyI_mJfi.js.gz +0 -0
- package/dist/dashboard/assets/banner-B-yXtCqb.js.gz +0 -0
- package/dist/dashboard/assets/brand/bansos_page.png +0 -0
- package/dist/dashboard/assets/brand/denia_bansos.jpg +0 -0
- package/dist/dashboard/assets/brand/favicon.jpg +0 -0
- package/dist/dashboard/assets/brand/login_register.jpg +0 -0
- package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
- package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
- package/dist/dashboard/assets/brand/og_image.jpg +0 -0
- package/dist/dashboard/assets/brand/sidebar_ico.jpg +0 -0
- package/dist/dashboard/assets/card-_HwAEqFF.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js +0 -1
- package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-ByjCk0o9.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BZhOlfz-.js.gz +0 -0
- package/dist/dashboard/assets/drawer-BADqIRYP.js.gz +0 -0
- package/dist/dashboard/assets/entry-COaC-irN.js.gz +0 -0
- package/dist/dashboard/assets/entry-Ca7LSCet.js +0 -4
- package/dist/dashboard/assets/entry-Ca7LSCet.js.gz +0 -0
- package/dist/dashboard/assets/entry-PDd27uCd.js +0 -1
- package/dist/dashboard/assets/entry-PDd27uCd.js.gz +0 -0
- package/dist/dashboard/assets/formatters-DcHJyRiu.js.gz +0 -0
- package/dist/dashboard/assets/index-CgKmFngv.js.gz +0 -0
- package/dist/dashboard/assets/inline-B2BirKry.js.gz +0 -0
- package/dist/dashboard/assets/input-D-qV9dKY.js.gz +0 -0
- package/dist/dashboard/assets/layout-B7tVWhtV.js.gz +0 -0
- package/dist/dashboard/assets/list-row-CtkGE3bF.js.gz +0 -0
- package/dist/dashboard/assets/live-DGizF06-.js.gz +0 -0
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +0 -1
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
- package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
- package/dist/dashboard/assets/network-C9UE-bOr.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BnAd22a_.js.gz +0 -0
- package/dist/dashboard/assets/pagination-BAywyMzS.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-YSoQOUh6.js.gz +0 -0
- package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
- package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
- package/dist/dashboard/assets/routing-fnCsxgNO.js +0 -1
- package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
- package/dist/dashboard/assets/select-BCOcv8WK.js.gz +0 -0
- package/dist/dashboard/assets/settings-BxslsErn.js +0 -1
- package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
- package/dist/dashboard/assets/stack-ByKt3Q1g.js.gz +0 -0
- package/dist/dashboard/assets/state-DXbGTx7N.js.gz +0 -0
- package/dist/dashboard/assets/switch-ZeTsW623.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
- package/dist/dashboard/console-backgrounds.jpg +0 -0
- package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
- package/dist/dashboard/donation/qris-risun-marke.png +0 -0
- package/src/providers/bundled-discovery-providers.ts +0 -98
- package/src/providers/configured-openai-providers.ts +0 -120
- package/src/providers/integrations/buddy/turing-device-token.ts +0 -262
- package/src/providers/integrations/buddy/workbuddy/cn-oauth.ts +0 -30
- package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +0 -58
- package/src/providers/integrations/buddy/workbuddy/oauth.ts +0 -31
- package/src/providers/integrations/buddy/workbuddy/provider.ts +0 -209
- package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -161
- package/src/providers/integrations/buddy/workbuddy/shared.ts +0 -285
- package/src/providers/operations/model-discovery-cache.ts +0 -34
- package/src/providers/operations/probe-fetch.ts +0 -15
- package/src/providers/operations/provider-version-cache.ts +0 -30
- package/src/providers/provider-capabilities.ts +0 -33
- package/src/providers/provider-credential-envelope.ts +0 -43
- /package/dist/dashboard/assets/{app-CTxoqyYB.js → app-DIXLjojg.js} +0 -0
- /package/dist/dashboard/assets/{app-CTxoqyYB.js.gz → app-DIXLjojg.js.gz} +0 -0
|
@@ -1,18 +1,66 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { healthEventBuffer } from "../../persistence/health-event-flush";
|
|
3
|
-
import { and, count, desc, eq, inArray, isNotNull, lte, sql } from "drizzle-orm";
|
|
3
|
+
import { and, count, desc, eq, inArray, isNotNull, like, lte, or, sql } from "drizzle-orm";
|
|
4
4
|
import { affectedRows, pruneExpiredCooldowns } from "../../persistence/dialect";
|
|
5
5
|
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
6
|
-
import { healthEvents, providerAccounts } from "../../persistence/schema";
|
|
6
|
+
import { consoleSettings, healthEvents, providerAccounts } from "../../persistence/schema";
|
|
7
7
|
import {
|
|
8
8
|
COOLDOWN_RECOVERY_BATCH,
|
|
9
9
|
accountDeadlineMs,
|
|
10
|
+
applyRuleEscalation,
|
|
10
11
|
classifyAccountError,
|
|
11
|
-
escalateFallbackCooldown,
|
|
12
12
|
latestDeadlineMs,
|
|
13
13
|
unsupportedModelCooldownMs,
|
|
14
14
|
} from "./account-error-classification";
|
|
15
|
+
import { resolveAccountDisabledRecoveryMs } from "../../config/cooldowns";
|
|
15
16
|
import type { AccountErrorClassification, AccountFailureEvidence } from "./account-error-classification";
|
|
17
|
+
import { resolveCooldownRule } from "./cooldown-rules";
|
|
18
|
+
import { mapPlanToTier } from "./account-tier";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Categories the auto-disable toggle (area 01) targets (D01). Model-scoped
|
|
22
|
+
* verdicts (`model_unreachable`) are not here — they belong to the tier rules
|
|
23
|
+
* (area 03, R-C).
|
|
24
|
+
*/
|
|
25
|
+
const AUTO_DISABLE_TARGET_CATEGORIES: ReadonlySet<string> = new Set([
|
|
26
|
+
"quota_exhausted",
|
|
27
|
+
"rate_limit_transient",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
/** Prefix marking an auto-disable written by the rate-limited intercept (D03/R-B). */
|
|
31
|
+
export const AUTO_DISABLE_MARKER_PREFIX = "auto-disable (rate-limited): ";
|
|
32
|
+
|
|
33
|
+
/** An upstream-stated window shorter than this is not auto-disabled (D04). */
|
|
34
|
+
const AUTO_DISABLE_STATED_MIN_MS = 5 * 60 * 1000;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether the auto-disable preference is on for a tenant (and for shared
|
|
38
|
+
* accounts — `tenantId === null` — when any tenant has it on, D06).
|
|
39
|
+
*
|
|
40
|
+
* The preference lives in `console_settings.preferences` (the C2 cluster added
|
|
41
|
+
* the typed field to `ConsoleSettingsPreferences`). Reading is fail-safe (D01
|
|
42
|
+
* §3.1): a missing row, a failed read, or a preference that is not exactly
|
|
43
|
+
* `true` all mean off — a settings failure must never break a health write.
|
|
44
|
+
*/
|
|
45
|
+
export async function autoDisableRateLimitedEnabled(
|
|
46
|
+
db: CartethyiaDatabase,
|
|
47
|
+
tenantId: string | null,
|
|
48
|
+
): Promise<boolean> {
|
|
49
|
+
try {
|
|
50
|
+
const rows = await db
|
|
51
|
+
.select({ tenantId: consoleSettings.tenantId, preferences: consoleSettings.preferences })
|
|
52
|
+
.from(consoleSettings);
|
|
53
|
+
return rows.some((row) => {
|
|
54
|
+
if (tenantId !== null && row.tenantId !== tenantId) return false;
|
|
55
|
+
const prefs = row.preferences as {
|
|
56
|
+
readonly autoDisableRateLimited?: boolean;
|
|
57
|
+
} | null | undefined;
|
|
58
|
+
return prefs?.autoDisableRateLimited === true;
|
|
59
|
+
});
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
16
64
|
|
|
17
65
|
export { classifyAccountError, unsupportedModelCooldownMs } from "./account-error-classification";
|
|
18
66
|
export type {
|
|
@@ -110,6 +158,11 @@ export async function recordAccountFailure(
|
|
|
110
158
|
// counted", which is the common case and must stay cheap.
|
|
111
159
|
modelBlockHits: providerAccounts.modelBlockHits,
|
|
112
160
|
cooldownUntil: providerAccounts.cooldownUntil,
|
|
161
|
+
// Required by the auto-disable toggle (tenant-level preference read)
|
|
162
|
+
// and by the tier-aware cooldown rules (plan → tier).
|
|
163
|
+
providerId: providerAccounts.providerId,
|
|
164
|
+
tenantId: providerAccounts.tenantId,
|
|
165
|
+
quotaPlan: providerAccounts.quotaPlan,
|
|
113
166
|
})
|
|
114
167
|
.from(providerAccounts)
|
|
115
168
|
.where(eq(providerAccounts.id, accountId))
|
|
@@ -128,6 +181,43 @@ export async function recordAccountFailure(
|
|
|
128
181
|
const fromStatus = account.status;
|
|
129
182
|
const now = new Date();
|
|
130
183
|
|
|
184
|
+
// ---- Auto-disable intercept (area 01, D01–D06) -------------------------
|
|
185
|
+
// Evaluated after classification and before the model-scoped/account-wide
|
|
186
|
+
// branch, so a 429/quota verdict carrying a modelId is rewritten to an
|
|
187
|
+
// account-wide disable instead of sinking into a per-model cooldown (D02).
|
|
188
|
+
//
|
|
189
|
+
// Only fires when every condition holds:
|
|
190
|
+
// - the tenant's toggle is on (fail-safe read: off on any error),
|
|
191
|
+
// - the classification mutates the account and the origin is upstream,
|
|
192
|
+
// - the category is a target (quota_exhausted / rate_limit_transient),
|
|
193
|
+
// - the verdict is a cooldown (rule-level `disabled` verdicts — bans,
|
|
194
|
+
// revoked credentials — are untouched),
|
|
195
|
+
// - the provider did not state a sub-5-minute window (D04: Inception's
|
|
196
|
+
// 1m limit must keep its short cooldown, not park the account for 24h).
|
|
197
|
+
const autoDisableTarget =
|
|
198
|
+
AUTO_DISABLE_TARGET_CATEGORIES.has(effective.category) &&
|
|
199
|
+
effective.modelScoped !== true &&
|
|
200
|
+
effective.status === "cooldown" &&
|
|
201
|
+
effective.mutatesAccount &&
|
|
202
|
+
options?.origin === "upstream";
|
|
203
|
+
const statedTooShort =
|
|
204
|
+
effective.providerStated === true &&
|
|
205
|
+
effective.cooldownMs > 0 &&
|
|
206
|
+
effective.cooldownMs < AUTO_DISABLE_STATED_MIN_MS;
|
|
207
|
+
let autoDisableActive = false;
|
|
208
|
+
if (autoDisableTarget && !statedTooShort) {
|
|
209
|
+
autoDisableActive = await autoDisableRateLimitedEnabled(client, account.tenantId);
|
|
210
|
+
}
|
|
211
|
+
// Rewritten classification used by the write paths below.
|
|
212
|
+
const interception = autoDisableActive
|
|
213
|
+
? {
|
|
214
|
+
...effective,
|
|
215
|
+
status: "disabled" as const,
|
|
216
|
+
cooldownMs: resolveAccountDisabledRecoveryMs(),
|
|
217
|
+
ruleSource: "fallback" as const,
|
|
218
|
+
}
|
|
219
|
+
: effective;
|
|
220
|
+
|
|
131
221
|
const modelId = options?.modelId;
|
|
132
222
|
// Every category here is a verdict about one (account, model) pair rather
|
|
133
223
|
// than about the credential, so all three back off that pair and leave the
|
|
@@ -138,16 +228,59 @@ export async function recordAccountFailure(
|
|
|
138
228
|
effective.category === "rate_limit_transient" ||
|
|
139
229
|
effective.category === "model_capacity" ||
|
|
140
230
|
effective.category === "model_unreachable";
|
|
141
|
-
|
|
231
|
+
// D02: when the auto-disable intercept fired, the target category must not
|
|
232
|
+
// sink into a model-scoped cooldown — the outcome is an account-wide
|
|
233
|
+
// disable.
|
|
234
|
+
const shouldModelCooldown =
|
|
235
|
+
!autoDisableActive &&
|
|
236
|
+
modelId !== undefined &&
|
|
237
|
+
(isThrottle || effective.modelScoped === true);
|
|
142
238
|
// The account-wide streak, escalated for a provider-stated window. Read once
|
|
143
239
|
// so the model-scoped and account-wide paths below cannot disagree about it:
|
|
144
240
|
// this is the count *before* this failure, which the UPDATE below increments.
|
|
145
241
|
const streak = Math.max(0, account.consecutiveFailures ?? 0);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
);
|
|
242
|
+
// Tier-aware rule resolution (area 03, D15–D19): the account's plan is
|
|
243
|
+
// projected on this row, mapped to a tier, and — when a rule exists for
|
|
244
|
+
// the (provider, category, tier) combination — replaces the classifier's
|
|
245
|
+
// duration. The rule duration is a policy decision and never escalates
|
|
246
|
+
// (D18); escalation applies only to the fallback.
|
|
247
|
+
const accountTier =
|
|
248
|
+
account.quotaPlan === null || account.quotaPlan === undefined
|
|
249
|
+
? null
|
|
250
|
+
: mapPlanToTier(account.providerId, account.quotaPlan);
|
|
251
|
+
// A provider-stated window always wins over a tier rule (D18), so the rule
|
|
252
|
+
// is only consulted when the upstream named nothing.
|
|
253
|
+
const ruleCooldownMs =
|
|
254
|
+
interception.providerStated === true
|
|
255
|
+
? undefined
|
|
256
|
+
: resolveCooldownRule({
|
|
257
|
+
providerId: account.providerId,
|
|
258
|
+
category: effective.category,
|
|
259
|
+
tier: accountTier,
|
|
260
|
+
});
|
|
261
|
+
const ruleSource = interception.ruleSource;
|
|
262
|
+
const sourceCooldownMs =
|
|
263
|
+
ruleCooldownMs !== undefined ? ruleCooldownMs : interception.cooldownMs;
|
|
264
|
+
const sourceRuleKind: "provider-rule" | "fallback" | undefined =
|
|
265
|
+
ruleCooldownMs !== undefined ? "provider-rule" : ruleSource;
|
|
266
|
+
// The auto-disable recovery window is a policy duration like a tier rule:
|
|
267
|
+
// it must never pass through the fallback escalation ladder, or the 1h cap
|
|
268
|
+
// would truncate the default 24h recovery on the first unstated 429 (D18).
|
|
269
|
+
const resolvedCooldownMs = autoDisableActive
|
|
270
|
+
? interception.cooldownMs
|
|
271
|
+
: applyRuleEscalation(
|
|
272
|
+
sourceCooldownMs,
|
|
273
|
+
interception.providerStated === true,
|
|
274
|
+
streak,
|
|
275
|
+
sourceRuleKind,
|
|
276
|
+
);
|
|
277
|
+
// When a rule replaces the duration, the classifier's `retryAt` (computed
|
|
278
|
+
// from the old constant/fallback) is stale: a shorter rule would lose to it
|
|
279
|
+
// in the monotonic-max deadline. Recompute from the rule duration instead.
|
|
280
|
+
const retryAtMs =
|
|
281
|
+
ruleCooldownMs !== undefined
|
|
282
|
+
? Date.now() + ruleCooldownMs
|
|
283
|
+
: effective.retryAt?.getTime();
|
|
151
284
|
|
|
152
285
|
// The unsupported-model ladder, computed before the branch: the classifier
|
|
153
286
|
// could only state the base window because the authoritative hit count
|
|
@@ -173,8 +306,8 @@ export async function recordAccountFailure(
|
|
|
173
306
|
// of the two whenever the provider really parked the pair longer.
|
|
174
307
|
const previousDeadline = existing[modelId] === undefined ? undefined : Date.parse(existing[modelId]);
|
|
175
308
|
const nextDeadline = latestDeadlineMs(
|
|
176
|
-
|
|
177
|
-
Date.now() + (unsupportedCooldownMs ??
|
|
309
|
+
retryAtMs,
|
|
310
|
+
Date.now() + (unsupportedCooldownMs ?? resolvedCooldownMs),
|
|
178
311
|
previousDeadline,
|
|
179
312
|
);
|
|
180
313
|
const nextUntil = new Date(nextDeadline).toISOString();
|
|
@@ -240,15 +373,35 @@ export async function recordAccountFailure(
|
|
|
240
373
|
// The route catalog projects modelCooldowns and stamps this candidate as
|
|
241
374
|
// model_cooldown. Invalidate when its deadline changes, or the cached
|
|
242
375
|
// snapshot keeps routing this pair until some unrelated write rebuilds it.
|
|
243
|
-
return {
|
|
376
|
+
return {
|
|
377
|
+
...classification,
|
|
378
|
+
routesVisible: previousDeadline !== nextDeadline,
|
|
379
|
+
...(sourceRuleKind === undefined ? {} : { ruleSource: sourceRuleKind }),
|
|
380
|
+
};
|
|
244
381
|
}
|
|
245
382
|
|
|
383
|
+
// When the intercept fired, the reason is prefixed with the auto-disable
|
|
384
|
+
// marker — `sweepExpiredCooldowns` (D03/R-B) uses the prefix to tell an
|
|
385
|
+
// auto-disable apart from an operator disable, which stays terminal.
|
|
386
|
+
const writtenReason = autoDisableActive
|
|
387
|
+
? `${AUTO_DISABLE_MARKER_PREFIX}${effective.reason}`
|
|
388
|
+
: effective.reason;
|
|
389
|
+
// A disabled account's deadline is the auto-disable recovery window — the
|
|
390
|
+
// classifier's `retryAt`/cooldown duration must not leak in, or a terminal
|
|
391
|
+
// disable (`recovery = 0`) would gain a deadline and be swept back.
|
|
392
|
+
const deadlineCandidates = autoDisableActive
|
|
393
|
+
? [interception.cooldownMs > 0 ? Date.now() + interception.cooldownMs : undefined]
|
|
394
|
+
: [
|
|
395
|
+
retryAtMs,
|
|
396
|
+
resolvedCooldownMs > 0 ? Date.now() + resolvedCooldownMs : undefined,
|
|
397
|
+
account.cooldownUntil?.getTime?.(),
|
|
398
|
+
];
|
|
246
399
|
await client
|
|
247
400
|
.update(providerAccounts)
|
|
248
401
|
.set({
|
|
249
|
-
status:
|
|
402
|
+
status: interception.status,
|
|
250
403
|
consecutiveFailures: sql`${providerAccounts.consecutiveFailures} + 1`,
|
|
251
|
-
lastError:
|
|
404
|
+
lastError: writtenReason,
|
|
252
405
|
lastErrorCategory: effective.category,
|
|
253
406
|
lastErrorAt: now,
|
|
254
407
|
// Monotonic against the account's own in-force deadline, for the same
|
|
@@ -259,11 +412,7 @@ export async function recordAccountFailure(
|
|
|
259
412
|
// (see `accountDeadlineMs`): the one state that asks for it is a
|
|
260
413
|
// terminal automatic disable, and an elapsed deadline there would hand
|
|
261
414
|
// the row straight back to the recovery sweep.
|
|
262
|
-
cooldownUntil: accountDeadlineMs(
|
|
263
|
-
effective.retryAt?.getTime(),
|
|
264
|
-
escalatedCooldownMs > 0 ? Date.now() + escalatedCooldownMs : undefined,
|
|
265
|
-
account.cooldownUntil?.getTime?.(),
|
|
266
|
-
]),
|
|
415
|
+
cooldownUntil: accountDeadlineMs(deadlineCandidates),
|
|
267
416
|
})
|
|
268
417
|
.where(eq(providerAccounts.id, accountId));
|
|
269
418
|
|
|
@@ -272,8 +421,8 @@ export async function recordAccountFailure(
|
|
|
272
421
|
entityKind: "account",
|
|
273
422
|
accountId,
|
|
274
423
|
fromStatus: fromStatus as "active" | "cooldown" | "disabled",
|
|
275
|
-
toStatus:
|
|
276
|
-
reason:
|
|
424
|
+
toStatus: interception.status,
|
|
425
|
+
reason: writtenReason,
|
|
277
426
|
errorCategory: effective.category,
|
|
278
427
|
modelId: modelId ?? null,
|
|
279
428
|
createdAt: now,
|
|
@@ -281,7 +430,11 @@ export async function recordAccountFailure(
|
|
|
281
430
|
}
|
|
282
431
|
// This path wrote `status` and `cooldownUntil`, both of which the snapshot
|
|
283
432
|
// projects, so routing has to see it.
|
|
284
|
-
return {
|
|
433
|
+
return {
|
|
434
|
+
...classification,
|
|
435
|
+
routesVisible: true,
|
|
436
|
+
...(sourceRuleKind === undefined ? {} : { ruleSource: sourceRuleKind }),
|
|
437
|
+
};
|
|
285
438
|
};
|
|
286
439
|
|
|
287
440
|
return withTransaction(db, persist, null);
|
|
@@ -414,11 +567,28 @@ export async function sweepExpiredCooldowns(db: CartethyiaDatabase): Promise<num
|
|
|
414
567
|
})
|
|
415
568
|
.from(providerAccounts)
|
|
416
569
|
.where(
|
|
417
|
-
// An ordinary cooldown, recovered as soon as its window elapses
|
|
570
|
+
// An ordinary cooldown, recovered as soon as its window elapses; plus
|
|
571
|
+
// (D03/R-B) an account the *auto-disable* intercept disabled — only
|
|
572
|
+
// when its category is a target AND the reason carries the auto-disable
|
|
573
|
+
// marker AND its recovery deadline has elapsed. An auto-disable is the
|
|
574
|
+
// machine's own verdict, so the sweep may undo it; an operator disable
|
|
575
|
+
// (no marker) stays terminal.
|
|
418
576
|
and(
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
577
|
+
or(
|
|
578
|
+
and(
|
|
579
|
+
eq(providerAccounts.status, "cooldown"),
|
|
580
|
+
isNotNull(providerAccounts.cooldownUntil),
|
|
581
|
+
lte(providerAccounts.cooldownUntil, now),
|
|
582
|
+
),
|
|
583
|
+
and(
|
|
584
|
+
eq(providerAccounts.status, "disabled"),
|
|
585
|
+
inArray(providerAccounts.lastErrorCategory, [...AUTO_DISABLE_TARGET_CATEGORIES]),
|
|
586
|
+
isNotNull(providerAccounts.lastError),
|
|
587
|
+
like(providerAccounts.lastError, `${AUTO_DISABLE_MARKER_PREFIX}%`),
|
|
588
|
+
isNotNull(providerAccounts.cooldownUntil),
|
|
589
|
+
lte(providerAccounts.cooldownUntil, now),
|
|
590
|
+
),
|
|
591
|
+
),
|
|
422
592
|
),
|
|
423
593
|
)
|
|
424
594
|
// One pass recovers a bounded batch. The sweep runs every 30s, so a
|
|
@@ -528,6 +698,83 @@ async function writeHealthEvent(
|
|
|
528
698
|
await client.insert(healthEvents).values({ id: randomUUID(), ...row });
|
|
529
699
|
}
|
|
530
700
|
|
|
701
|
+
/**
|
|
702
|
+
* The one internal seam that applies a health verdict to an account row.
|
|
703
|
+
*
|
|
704
|
+
* Every producer that persists account-state semantics — dispatch failure
|
|
705
|
+
* classification, OAuth refresh definitive failure, quota refresh parking —
|
|
706
|
+
* converges here instead of writing `status`/`cooldownUntil`/error fields
|
|
707
|
+
* independently. Producers may still classify differently; they may not
|
|
708
|
+
* persist account-state semantics separately.
|
|
709
|
+
*
|
|
710
|
+
* This writes the account-wide health state plus a health audit row, inside the
|
|
711
|
+
* caller's transaction (`client`). It returns `true` when the account row was
|
|
712
|
+
* actually updated (i.e. the account exists and is not operator-disabled with
|
|
713
|
+
* `preserveOperatorDisable`), so the caller knows whether routing must be
|
|
714
|
+
* invalidated (routesVisible) or the lease released.
|
|
715
|
+
*/
|
|
716
|
+
export async function applyAccountVerdict(
|
|
717
|
+
client: CartethyiaDatabase,
|
|
718
|
+
verdict: {
|
|
719
|
+
readonly accountId: string;
|
|
720
|
+
readonly status: "cooldown" | "disabled";
|
|
721
|
+
readonly reason: string;
|
|
722
|
+
readonly category: string | null;
|
|
723
|
+
readonly cooldownUntil?: Date | null;
|
|
724
|
+
/** When true, an operator-disable (status `disabled`, no deadline) is left untouched. */
|
|
725
|
+
readonly preserveOperatorDisable?: boolean;
|
|
726
|
+
/** Health event transition status (defaults to the verdict status). */
|
|
727
|
+
readonly fromStatus?: "active" | "cooldown" | "disabled" | null;
|
|
728
|
+
readonly modelId?: string | null;
|
|
729
|
+
readonly createdAt?: Date;
|
|
730
|
+
},
|
|
731
|
+
): Promise<boolean> {
|
|
732
|
+
return withTransaction(
|
|
733
|
+
client,
|
|
734
|
+
async (tx) => {
|
|
735
|
+
const rows = await tx
|
|
736
|
+
.select({
|
|
737
|
+
status: providerAccounts.status,
|
|
738
|
+
cooldownUntil: providerAccounts.cooldownUntil,
|
|
739
|
+
})
|
|
740
|
+
.from(providerAccounts)
|
|
741
|
+
.where(eq(providerAccounts.id, verdict.accountId))
|
|
742
|
+
.limit(1);
|
|
743
|
+
const row = rows[0];
|
|
744
|
+
if (!row) return false;
|
|
745
|
+
if (verdict.preserveOperatorDisable === true) {
|
|
746
|
+
const isOperatorDisable = row.status === "disabled" && row.cooldownUntil == null;
|
|
747
|
+
if (isOperatorDisable) return false;
|
|
748
|
+
}
|
|
749
|
+
const now = verdict.createdAt ?? new Date();
|
|
750
|
+
await tx
|
|
751
|
+
.update(providerAccounts)
|
|
752
|
+
.set({
|
|
753
|
+
status: verdict.status,
|
|
754
|
+
lastError: (verdict.reason ?? "Account failed").slice(0, 500),
|
|
755
|
+
lastErrorCategory: verdict.category ?? "check_failed",
|
|
756
|
+
lastErrorAt: now,
|
|
757
|
+
...(verdict.cooldownUntil === undefined
|
|
758
|
+
? {}
|
|
759
|
+
: { cooldownUntil: verdict.cooldownUntil }),
|
|
760
|
+
})
|
|
761
|
+
.where(eq(providerAccounts.id, verdict.accountId));
|
|
762
|
+
await writeHealthEvent(tx, {
|
|
763
|
+
entityKind: "account",
|
|
764
|
+
accountId: verdict.accountId,
|
|
765
|
+
fromStatus: verdict.fromStatus ?? (row.status as "active" | "cooldown" | "disabled" | null),
|
|
766
|
+
toStatus: verdict.status,
|
|
767
|
+
reason: verdict.reason,
|
|
768
|
+
errorCategory: verdict.category,
|
|
769
|
+
modelId: verdict.modelId ?? null,
|
|
770
|
+
createdAt: now,
|
|
771
|
+
});
|
|
772
|
+
return true;
|
|
773
|
+
},
|
|
774
|
+
false,
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
|
|
531
778
|
export async function reportAttemptOutcome(
|
|
532
779
|
db: CartethyiaDatabase,
|
|
533
780
|
report: AttemptHealthReport,
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manual account-status observation: refresh an OAuth token on demand and
|
|
3
|
+
* report the account's status AFTER the attempt — never assume it.
|
|
4
|
+
*
|
|
5
|
+
* Area 04 (D21–D26): a refresh is not a health verdict. This module is the
|
|
6
|
+
* explicit "refresh token" affordance for accounts the OAuth sweep excludes
|
|
7
|
+
* (`disabled`), and it is where the invariant nobody else had to is held:
|
|
8
|
+
* `statusAfter` is always re-read from the database, so no caller can ever
|
|
9
|
+
* report `"active"` just because a refresh succeeded.
|
|
10
|
+
*/
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
13
|
+
import { healthEvents } from "../../persistence/schema";
|
|
14
|
+
import { pushStructuredConsoleLog } from "../../observability/log-ring";
|
|
15
|
+
import { loadAccountWithOauthState } from "./provider-credential-service";
|
|
16
|
+
import type { OAuthRefreshService, OAuthTokenRefresher } from "../authentication/oauth-refresh-service";
|
|
17
|
+
|
|
18
|
+
export type RefreshAccountStatusCode =
|
|
19
|
+
| "ok"
|
|
20
|
+
| "account_missing"
|
|
21
|
+
| "not_oauth"
|
|
22
|
+
| "static_token"
|
|
23
|
+
| "no_refresher"
|
|
24
|
+
| "refresh_failed";
|
|
25
|
+
|
|
26
|
+
export interface RefreshAccountStatusOutcome {
|
|
27
|
+
readonly ok: boolean;
|
|
28
|
+
readonly tokenRefreshed: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Account status re-read from the DB after the attempt. Never assumed from
|
|
31
|
+
* the input row and never `"active"` by fiat — a refresh cannot re-enable.
|
|
32
|
+
*/
|
|
33
|
+
readonly statusAfter: string;
|
|
34
|
+
readonly code: RefreshAccountStatusCode;
|
|
35
|
+
/** IS0 timestamp of the fresh access-token expiry, when known. */
|
|
36
|
+
readonly expiresAt?: string;
|
|
37
|
+
/** ISO timestamp of the attempt, when it happened. */
|
|
38
|
+
readonly refreshedAt?: string;
|
|
39
|
+
/** Structured reason when `ok` is false (`not_oauth`, `static_token`, …). */
|
|
40
|
+
readonly error?: string;
|
|
41
|
+
/** Human detail (failure message) when available. */
|
|
42
|
+
readonly category?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RefreshAccountStatusDeps {
|
|
46
|
+
readonly db: CartethyiaDatabase;
|
|
47
|
+
readonly refreshService: Pick<OAuthRefreshService, "ensureFreshAccessToken">;
|
|
48
|
+
readonly resolveRefresher: (providerId: string) => Promise<OAuthTokenRefresher | undefined>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface LoadedAccount {
|
|
52
|
+
readonly id: string;
|
|
53
|
+
readonly providerId: string;
|
|
54
|
+
readonly credentialKind: string;
|
|
55
|
+
readonly status: string;
|
|
56
|
+
readonly staticToken: boolean;
|
|
57
|
+
readonly refreshCiphertext: Buffer | null;
|
|
58
|
+
readonly expiresAt: Date | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function loadAccount(
|
|
62
|
+
db: CartethyiaDatabase,
|
|
63
|
+
accountId: string,
|
|
64
|
+
): Promise<LoadedAccount | undefined> {
|
|
65
|
+
const loaded = await loadAccountWithOauthState(db, accountId, {
|
|
66
|
+
includeStatus: true,
|
|
67
|
+
includeExpiry: true,
|
|
68
|
+
});
|
|
69
|
+
if (!loaded || loaded.status === undefined || loaded.expiresAt === undefined) return undefined;
|
|
70
|
+
return {
|
|
71
|
+
id: loaded.id,
|
|
72
|
+
providerId: loaded.providerId,
|
|
73
|
+
credentialKind: loaded.credentialKind,
|
|
74
|
+
status: loaded.status,
|
|
75
|
+
staticToken: loaded.staticToken,
|
|
76
|
+
refreshCiphertext: loaded.refreshCiphertext,
|
|
77
|
+
expiresAt: loaded.expiresAt,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function readStatusAfter(
|
|
82
|
+
db: CartethyiaDatabase,
|
|
83
|
+
accountId: string,
|
|
84
|
+
): Promise<{ status: string; expiresAt: Date | null } | undefined> {
|
|
85
|
+
const loaded = await loadAccountWithOauthState(db, accountId, {
|
|
86
|
+
includeStatus: true,
|
|
87
|
+
includeExpiry: true,
|
|
88
|
+
});
|
|
89
|
+
if (!loaded || loaded.status === undefined || loaded.expiresAt === undefined) return undefined;
|
|
90
|
+
return { status: loaded.status, expiresAt: loaded.expiresAt };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Non-mutating health event: records the attempt, never changes status. */
|
|
94
|
+
async function writeRefreshHealthEvent(
|
|
95
|
+
db: CartethyiaDatabase,
|
|
96
|
+
accountId: string,
|
|
97
|
+
fromStatus: string,
|
|
98
|
+
toStatus: string,
|
|
99
|
+
reason: string,
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
if (typeof db.insert !== "function") return;
|
|
102
|
+
await db.insert(healthEvents).values({
|
|
103
|
+
id: randomUUID(),
|
|
104
|
+
entityKind: "account",
|
|
105
|
+
accountId,
|
|
106
|
+
fromStatus: fromStatus as "active" | "cooldown" | "disabled" | null,
|
|
107
|
+
toStatus: toStatus as "active" | "cooldown" | "disabled",
|
|
108
|
+
reason,
|
|
109
|
+
errorCategory: null,
|
|
110
|
+
createdAt: new Date(),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Refreshes an OAuth account's token on demand and reports the resulting
|
|
116
|
+
* status. Works for any status, `disabled` included — the account stays
|
|
117
|
+
* `disabled`; only an operator can re-enable it.
|
|
118
|
+
*
|
|
119
|
+
* Rejections are structured (`{ ok: false, code: "not_oauth" | "static_token" |
|
|
120
|
+
* ... }`), and every outcome carries `statusAfter` re-read from the DB.
|
|
121
|
+
*/
|
|
122
|
+
export async function refreshAccountStatus(
|
|
123
|
+
deps: RefreshAccountStatusDeps,
|
|
124
|
+
accountId: string,
|
|
125
|
+
opts: { readonly origin?: "ui" | "sweep" | "dispatch" } = {},
|
|
126
|
+
): Promise<RefreshAccountStatusOutcome> {
|
|
127
|
+
const origin = opts.origin ?? "ui";
|
|
128
|
+
const account = await loadAccount(deps.db, accountId);
|
|
129
|
+
if (!account) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
tokenRefreshed: false,
|
|
133
|
+
statusAfter: "disabled",
|
|
134
|
+
code: "account_missing",
|
|
135
|
+
error: "Account not found",
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const statusBefore = account.status;
|
|
139
|
+
|
|
140
|
+
if (account.credentialKind !== "oauth") {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
tokenRefreshed: false,
|
|
144
|
+
statusAfter: statusBefore,
|
|
145
|
+
code: "not_oauth",
|
|
146
|
+
error: "not_oauth",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (account.staticToken || account.refreshCiphertext == null) {
|
|
150
|
+
// No refresh grant (or an explicit static marker): there is nothing to
|
|
151
|
+
// re-mint, and forcing the service to try would mutate the row into a
|
|
152
|
+
// static token. Refuse instead of changing the account.
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
tokenRefreshed: false,
|
|
156
|
+
statusAfter: statusBefore,
|
|
157
|
+
code: "static_token",
|
|
158
|
+
error: "static_token",
|
|
159
|
+
...(account.refreshCiphertext == null && !account.staticToken
|
|
160
|
+
? { category: "No refresh token on record" }
|
|
161
|
+
: {}),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const refresher = await deps.resolveRefresher(account.providerId);
|
|
166
|
+
if (!refresher) {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
tokenRefreshed: false,
|
|
170
|
+
statusAfter: statusBefore,
|
|
171
|
+
code: "no_refresher",
|
|
172
|
+
error: "no_refresher",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// `ensureFreshAccessToken` deliberately swallows a failing refresher and
|
|
177
|
+
// returns null (it must not break a dispatch). For this explicit affordance
|
|
178
|
+
// the operator needs the reason, so keep the last failure message here
|
|
179
|
+
// rather than generalizing it — same "message as-is" rule as area 08.
|
|
180
|
+
let refreshFailureMessage: string | undefined;
|
|
181
|
+
const reportingRefresher: OAuthTokenRefresher = {
|
|
182
|
+
refresh: async (...args) => {
|
|
183
|
+
try {
|
|
184
|
+
return await refresher.refresh(...args);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
refreshFailureMessage = error instanceof Error ? error.message : String(error);
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const startedAt = new Date();
|
|
193
|
+
try {
|
|
194
|
+
const refreshed = await deps.refreshService.ensureFreshAccessToken(accountId, reportingRefresher, {
|
|
195
|
+
force: true,
|
|
196
|
+
origin,
|
|
197
|
+
});
|
|
198
|
+
// D25: status is always re-read after the attempt — never assumed.
|
|
199
|
+
const after = await readStatusAfter(deps.db, accountId);
|
|
200
|
+
const statusAfter = after?.status ?? statusBefore;
|
|
201
|
+
if (refreshed !== null) {
|
|
202
|
+
pushStructuredConsoleLog("info", "OAuth token refreshed", {
|
|
203
|
+
event: "token_refresh",
|
|
204
|
+
accountId,
|
|
205
|
+
providerId: account.providerId,
|
|
206
|
+
details: {
|
|
207
|
+
origin,
|
|
208
|
+
statusBefore,
|
|
209
|
+
statusAfter,
|
|
210
|
+
expiresAt: after?.expiresAt?.toISOString() ?? "",
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
await writeRefreshHealthEvent(
|
|
214
|
+
deps.db,
|
|
215
|
+
accountId,
|
|
216
|
+
statusBefore,
|
|
217
|
+
statusAfter,
|
|
218
|
+
"Token refreshed — status unchanged",
|
|
219
|
+
);
|
|
220
|
+
return {
|
|
221
|
+
ok: true,
|
|
222
|
+
tokenRefreshed: true,
|
|
223
|
+
statusAfter,
|
|
224
|
+
code: "ok",
|
|
225
|
+
...(after?.expiresAt ? { expiresAt: after.expiresAt.toISOString() } : {}),
|
|
226
|
+
refreshedAt: startedAt.toISOString(),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
// `ensureFreshAccessToken` returned null: the refresh failed (transient
|
|
230
|
+
// upstream error, lease loss) or the credential became static mid-flight.
|
|
231
|
+
// Either way the account's own status is the truth, re-read above.
|
|
232
|
+
pushStructuredConsoleLog("warn", "OAuth token refresh did not complete", {
|
|
233
|
+
event: "token_refresh",
|
|
234
|
+
accountId,
|
|
235
|
+
providerId: account.providerId,
|
|
236
|
+
errorCode: "refresh_failed",
|
|
237
|
+
details: { origin, statusBefore, statusAfter },
|
|
238
|
+
});
|
|
239
|
+
return {
|
|
240
|
+
ok: false,
|
|
241
|
+
tokenRefreshed: false,
|
|
242
|
+
statusAfter,
|
|
243
|
+
code: "refresh_failed",
|
|
244
|
+
error: "refresh_failed",
|
|
245
|
+
...(refreshFailureMessage
|
|
246
|
+
? { category: refreshFailureMessage.slice(0, 500) }
|
|
247
|
+
: { category: "Token refresh did not complete" }),
|
|
248
|
+
};
|
|
249
|
+
} catch (error) {
|
|
250
|
+
const after = await readStatusAfter(deps.db, accountId);
|
|
251
|
+
const statusAfter = after?.status ?? statusBefore;
|
|
252
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
253
|
+
pushStructuredConsoleLog("error", "OAuth token refresh failed", {
|
|
254
|
+
event: "token_refresh",
|
|
255
|
+
accountId,
|
|
256
|
+
providerId: account.providerId,
|
|
257
|
+
errorCode: "refresh_failed",
|
|
258
|
+
details: { origin, statusBefore, statusAfter, error: message.slice(0, 500) },
|
|
259
|
+
});
|
|
260
|
+
return {
|
|
261
|
+
ok: false,
|
|
262
|
+
tokenRefreshed: false,
|
|
263
|
+
statusAfter,
|
|
264
|
+
code: "refresh_failed",
|
|
265
|
+
error: "refresh_failed",
|
|
266
|
+
category: message.slice(0, 500),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|