cartethyia 2.7.9 → 2.8.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 +6 -5
- package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Dq530awD.js} +1 -1
- package/dist/dashboard/assets/BannedPage-Dq530awD.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-DZzaS-7S.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DZzaS-7S.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-DaZZ3cqP.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DaZZ3cqP.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BlC_xdkX.js} +1 -1
- package/dist/dashboard/assets/CombosPage-BlC_xdkX.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-D-rqeVtG.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-D-rqeVtG.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-7xT5yg2E.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-7xT5yg2E.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js.gz +0 -0
- package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-6Teoq9Se.js} +2 -2
- package/dist/dashboard/assets/InjectSystemPromptPanel-6Teoq9Se.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CZuI0MAR.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CZuI0MAR.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-C1hNTlkr.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-C1hNTlkr.js.gz +0 -0
- package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-0yfhSI1C.js} +1 -1
- package/dist/dashboard/assets/OverviewPage-0yfhSI1C.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js +8 -0
- package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js.gz +0 -0
- package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-Ddhm1InL.js} +1 -1
- package/dist/dashboard/assets/ProviderIcon-Ddhm1InL.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js +9 -0
- package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js +1 -0
- package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-BzZAvYSX.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-BzZAvYSX.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage--s-EfEIL.js} +1 -1
- package/dist/dashboard/assets/SetupPage--s-EfEIL.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-BjXFGSK4.js} +1 -1
- package/dist/dashboard/assets/StudioPage-BjXFGSK4.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-CP8F8BN7.js} +1 -1
- package/dist/dashboard/assets/UsagePage-CP8F8BN7.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-2hc12Uz2.js} +1 -1
- package/dist/dashboard/assets/api-keys-2hc12Uz2.js.gz +0 -0
- package/dist/dashboard/assets/app-DGXbNfAR.css +2 -0
- package/dist/dashboard/assets/app-DGXbNfAR.css.gz +0 -0
- package/dist/dashboard/assets/assert-BMcj1xZE.js +1 -0
- package/dist/dashboard/assets/assert-BMcj1xZE.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-DkRUuVfz.js} +1 -1
- package/dist/dashboard/assets/badge-DkRUuVfz.js.gz +0 -0
- package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-CDjW841Q.js} +1 -1
- package/dist/dashboard/assets/banner-CDjW841Q.js.gz +0 -0
- package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-Bto5UlL9.js} +1 -1
- package/dist/dashboard/assets/card-Bto5UlL9.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-DaH6g9hL.js} +1 -1
- package/dist/dashboard/assets/cli-tools-DaH6g9hL.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-BYKBCzjk.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-BYKBCzjk.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-DL0Zftwv.js} +1 -1
- package/dist/dashboard/assets/dialog-DL0Zftwv.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-Dpx_UXaS.js} +1 -1
- package/dist/dashboard/assets/drawer-Dpx_UXaS.js.gz +0 -0
- package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-BV7-1NoM.js} +1 -1
- package/dist/dashboard/assets/entry-BV7-1NoM.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-CGvdNU6r.js} +1 -1
- package/dist/dashboard/assets/entry-CGvdNU6r.js.gz +0 -0
- package/dist/dashboard/assets/entry-DScKYvZj.js +4 -0
- package/dist/dashboard/assets/entry-DScKYvZj.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-BS3DZlZj.js} +1 -1
- package/dist/dashboard/assets/formatters-BS3DZlZj.js.gz +0 -0
- package/dist/dashboard/assets/{index-BU64Lqvp.js → index-SQnvvTWX.js} +2 -2
- package/dist/dashboard/assets/index-SQnvvTWX.js.gz +0 -0
- package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-Dl-cuLK0.js} +1 -1
- package/dist/dashboard/assets/inline-Dl-cuLK0.js.gz +0 -0
- package/dist/dashboard/assets/{input-BCVkdR9d.js → input-Bfx0EzRc.js} +1 -1
- package/dist/dashboard/assets/input-Bfx0EzRc.js.gz +0 -0
- package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-BxhbQ_JS.js} +1 -1
- package/dist/dashboard/assets/layout-BxhbQ_JS.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-D49Y4ptn.js} +1 -1
- package/dist/dashboard/assets/list-row-D49Y4ptn.js.gz +0 -0
- package/dist/dashboard/assets/{live-Cae01iu8.js → live-CbVTdX9V.js} +1 -1
- package/dist/dashboard/assets/live-CbVTdX9V.js.gz +0 -0
- package/dist/dashboard/assets/{network-CRib7H_2.js → network-JGvDveof.js} +1 -1
- package/dist/dashboard/assets/network-JGvDveof.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-Dw6kiew-.js} +1 -1
- package/dist/dashboard/assets/numbers-Dw6kiew-.js.gz +0 -0
- package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-iUoI8GeE.js} +1 -1
- package/dist/dashboard/assets/pagination-iUoI8GeE.js.gz +0 -0
- package/dist/dashboard/assets/providers/sensenova-intl.png +0 -0
- package/dist/dashboard/assets/providers/sensenova.png +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/quota-DiPD6UKH.js +1 -0
- package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
- package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-8jZr-3WE.js} +1 -1
- package/dist/dashboard/assets/routing-8jZr-3WE.js.gz +0 -0
- package/dist/dashboard/assets/{select-ChoFWyKz.js → select-oO1FxMVb.js} +2 -2
- package/dist/dashboard/assets/select-oO1FxMVb.js.gz +0 -0
- package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-CLr3kdIx.js} +1 -1
- package/dist/dashboard/assets/settings-CLr3kdIx.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-Y3pDteZr.js} +1 -1
- package/dist/dashboard/assets/stack-Y3pDteZr.js.gz +0 -0
- package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-CArX_Iak.js} +1 -1
- package/dist/dashboard/assets/state-CArX_Iak.js.gz +0 -0
- package/dist/dashboard/assets/{switch-Db70BMix.js → switch-DwlwmDO-.js} +1 -1
- package/dist/dashboard/assets/switch-DwlwmDO-.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-DJ9GYEgf.js} +1 -1
- package/dist/dashboard/assets/useMutation-DJ9GYEgf.js.gz +0 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +54 -53
- package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
- package/migrations/full-postgres/0047_add_provider_device_flow_proxy.sql +17 -0
- package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
- package/migrations/lite-sqlite3/0047_add_provider_device_flow_proxy.sql +19 -0
- package/package.json +1 -1
- package/src/boot/app.ts +11 -14
- package/src/boot/dependencies.ts +5 -7
- package/src/boot/flush.ts +20 -0
- package/src/boot/lifecycle.ts +7 -5
- package/src/config/providers.ts +6 -5
- package/src/config/routing.ts +26 -0
- package/src/config/spec-metadata.ts +72 -0
- package/src/config/spec.ts +28 -8
- package/src/console/api-keys/contracts.ts +12 -1
- package/src/console/api-keys/routes.ts +40 -28
- 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/backup/nine-router.ts +71 -10
- package/src/console/backup/service.ts +8 -1
- package/src/console/cli-tools/service.ts +6 -12
- package/src/console/console-router-guards.ts +23 -16
- package/src/console/console-router.ts +1 -1
- package/src/console/domain-registration.ts +7 -4
- package/src/console/inferhub/client.ts +1 -1
- package/src/console/inferhub/normalize.ts +24 -28
- package/src/console/inferhub/types.ts +3 -0
- package/src/console/ip-bans/routes.ts +7 -13
- package/src/console/observability/live.ts +2 -2
- package/src/console/observability/store.ts +0 -53
- package/src/console/providers/catalog/contracts.ts +9 -3
- package/src/console/providers/catalog/model-operations.ts +2 -3
- package/src/console/providers/catalog/provider-operations.ts +1 -1
- package/src/console/providers/catalog/routes.ts +3 -2
- package/src/console/providers/catalog/store.ts +1914 -1902
- package/src/console/providers/detail/routes.ts +1 -0
- package/src/console/providers/detail/store.ts +4 -0
- package/src/console/providers/oauth/callback-listener.ts +30 -11
- package/src/console/providers/oauth/device-egress.ts +123 -0
- package/src/console/providers/oauth/routes.ts +123 -16
- package/src/console/quota/account-quota-global.ts +14 -24
- package/src/console/quota/account-quota-shared.ts +49 -0
- package/src/console/quota/account-quota-tenant.ts +11 -23
- package/src/console/quota/account-test-egress.ts +2 -2
- package/src/console/quota/daily-growth-job.ts +1 -1
- package/src/console/quota/quota-view.ts +19 -3
- package/src/console/routing/pools/contracts.ts +1 -1
- package/src/console/routing/pools/operations.ts +32 -47
- package/src/console/routing/pools/probe-verdict.ts +2 -2
- package/src/console/routing/pools/relay-deploy.ts +1 -1
- package/src/console/routing/pools/store.ts +4 -4
- package/src/console/shared/errors.ts +33 -6
- package/src/main.ts +2 -3
- package/src/network/client.ts +67 -0
- package/src/network/egress/bidi-session.ts +322 -0
- package/src/network/egress/dial.ts +201 -0
- package/src/network/egress/factory.ts +191 -0
- package/src/network/{outbound-fetch.ts → egress/fetch.ts} +195 -29
- package/src/network/egress/tls-profile.ts +123 -0
- package/src/network/egress/ws.ts +175 -0
- package/src/network/pool/{byte-accounting.ts → accounting.ts} +3 -2
- package/src/network/{types.ts → pool/agent-config.ts} +1 -1
- package/src/network/pool/agent.ts +2 -2
- package/src/network/{pool-cooldown-store.ts → pool/cooldown-store.ts} +95 -20
- package/src/network/{pool-health-machine.ts → pool/health.ts} +205 -6
- package/src/network/pool/resolver.ts +10 -849
- package/src/network/pool/selector.ts +184 -41
- package/src/network/ssrf.ts +7 -7
- package/src/network/{retry.ts → util/retry.ts} +1 -1
- package/src/network/{rate-limit-verdict.ts → verdict.ts} +1 -1
- package/src/observability/event-log.ts +7 -361
- package/src/observability/performance-metrics.ts +9 -0
- package/src/observability/runtime-metrics.ts +14 -9
- package/src/observability/telemetry-buffer.ts +16 -26
- 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 +18 -0
- package/src/persistence/migrate.ts +6 -3
- package/src/persistence/schema.ts +17 -0
- package/src/persistence/ttl-cache.ts +21 -3
- package/src/protocol/codecs.ts +59 -2
- package/src/protocol/dsml.ts +34 -86
- package/src/protocol/markup-stream-parser.ts +195 -0
- package/src/protocol/reasoning-text.ts +19 -0
- package/src/protocol/request/chat.ts +10 -10
- package/src/protocol/request/gemini.ts +8 -2
- package/src/protocol/request/messages.ts +34 -22
- package/src/protocol/response/chat.ts +12 -20
- package/src/protocol/response/messages.ts +34 -7
- package/src/protocol/response/omp-tool-call-markup.ts +43 -86
- package/src/protocol/response/responses.ts +18 -2
- package/src/protocol/stop-reason.ts +23 -1
- package/src/protocol/think-split.ts +112 -0
- package/src/protocol/translation/dialect/qwen.ts +26 -58
- package/src/protocol/translation/quirks.ts +15 -0
- package/src/protocol/upstream/send-claude-messages.ts +2 -2
- package/src/providers/adapters/compatible-adapter.ts +76 -12
- package/src/providers/authentication/oauth-client.ts +40 -6
- package/src/providers/authentication/oauth-device-flow.ts +105 -11
- package/src/providers/authentication/oauth-flow-store.ts +82 -1
- package/src/providers/authentication/oauth-refresh-service.ts +766 -758
- package/src/providers/configured-provider.ts +148 -0
- package/src/providers/default-registry.ts +137 -36
- package/src/providers/discovery/discovery-types.ts +7 -0
- package/src/providers/discovery/probe-phases.ts +20 -6
- package/src/providers/discovery/probing-service.ts +22 -2
- 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 +16 -1
- package/src/providers/integrations/buddy/account.ts +969 -0
- package/src/providers/integrations/buddy/capabilities.ts +12 -12
- package/src/providers/integrations/buddy/{buddy-catalog-shared.ts → catalog.ts} +198 -136
- package/src/providers/integrations/buddy/{buddy-chat-shared.ts → chat.ts} +176 -20
- package/src/providers/integrations/buddy/codebuddy/index.ts +429 -0
- package/src/providers/integrations/buddy/codebuddy/shared.ts +41 -26
- package/src/providers/integrations/buddy/headers.ts +311 -0
- package/src/providers/integrations/buddy/lifecycle.ts +1018 -0
- package/src/providers/integrations/buddy/{buddy-media.ts → media.ts} +1 -1
- package/src/providers/integrations/claude/claude-quota.ts +14 -17
- package/src/providers/integrations/cline/cline-oauth.ts +10 -5
- package/src/providers/integrations/cline/cline.ts +5 -6
- package/src/providers/integrations/clixad/clixad-oauth.ts +98 -0
- package/src/providers/integrations/clixad/clixad-quota.ts +132 -0
- package/src/providers/integrations/clixad/provider.ts +145 -0
- package/src/providers/integrations/codex/codex-oauth.ts +8 -5
- package/src/providers/integrations/commandcode/quota.ts +22 -28
- package/src/providers/integrations/cursor/cursor-auth.ts +1 -9
- package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
- package/src/providers/integrations/cursor/cursor-hardening.ts +279 -0
- package/src/providers/integrations/cursor/cursor-http2.ts +605 -6
- package/src/providers/integrations/cursor/cursor-request.ts +26 -12
- package/src/providers/integrations/cursor/cursor-shared.ts +8 -5
- package/src/providers/integrations/cursor/cursor.ts +574 -110
- 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/deepai/deepai-catalog.ts +149 -0
- package/src/providers/integrations/deepai/deepai-common.ts +145 -0
- package/src/providers/integrations/deepai/deepai-oauth.ts +129 -0
- package/src/providers/integrations/deepai/deepai-quota.ts +97 -0
- package/src/providers/integrations/deepai/provider.ts +42 -0
- package/src/providers/integrations/devin/devin-auth.ts +7 -0
- package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
- package/src/providers/integrations/factory/factory-oauth.ts +33 -51
- 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 +5 -0
- package/src/providers/integrations/freepi/freepi-discovery.ts +2 -2
- package/src/providers/integrations/freepi/freepi-headers.ts +2 -2
- package/src/providers/integrations/freepi/freepi-protocol.ts +11 -2
- package/src/providers/integrations/freepi/freepi-realm.ts +0 -3
- package/src/providers/integrations/freepi/freepi-session.ts +158 -0
- package/src/providers/integrations/freepi/freepi.ts +80 -54
- package/src/providers/integrations/github/github-discovery.ts +30 -13
- package/src/providers/integrations/github/github-oauth.ts +217 -44
- package/src/providers/integrations/github/github-quota.ts +22 -5
- package/src/providers/integrations/github/github.ts +40 -32
- package/src/providers/integrations/grok/grok-oauth.ts +52 -49
- 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 +3 -3
- package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +42 -13
- package/src/providers/integrations/hermes/index.ts +2 -0
- package/src/providers/integrations/kilo/kilo-oauth.ts +4 -4
- package/src/providers/integrations/kilo/kilo.ts +1 -1
- package/src/providers/integrations/kimi/kimi-oauth.ts +3 -3
- 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-oauth.ts +2 -2
- package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
- package/src/providers/integrations/kiro/kiro.ts +15 -19
- package/src/providers/integrations/muse/muse-oauth.ts +5 -5
- 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/openrouter/provider.ts +1 -1
- package/src/providers/integrations/perch/perch-credential.ts +25 -0
- package/src/providers/integrations/perch/perch-protocol.ts +0 -6
- package/src/providers/integrations/perch/perch-quota.ts +2 -16
- package/src/providers/integrations/perch/perch.ts +6 -16
- package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
- package/src/providers/integrations/qoder/qoder-adapter.ts +20 -125
- package/src/providers/integrations/qoder/qoder-auth.ts +109 -30
- package/src/providers/integrations/qoder/qoder-catalog.ts +21 -6
- package/src/providers/integrations/qoder/qoder-chat.ts +32 -29
- package/src/providers/integrations/qoder/qoder-checkin.ts +5 -5
- package/src/providers/integrations/qoder/qoder-cosy.ts +37 -9
- package/src/providers/integrations/qoder/qoder-oauth.ts +9 -2
- package/src/providers/integrations/qoder/qoder-realm.ts +13 -36
- package/src/providers/integrations/qoder/qoder-sse.ts +447 -68
- package/src/providers/integrations/qoder/qoder-types.ts +9 -0
- package/src/providers/integrations/qoder/qoder.ts +3 -26
- package/src/providers/integrations/sensenova/provider.ts +144 -0
- package/src/providers/integrations/trae/trae-protocol.ts +9 -5
- package/src/providers/integrations/trae/trae-quota.ts +21 -29
- package/src/providers/integrations/trae/trae-realm.ts +38 -0
- package/src/providers/integrations/trae/trae.ts +7 -24
- package/src/providers/integrations/vercel/provider.ts +117 -0
- package/src/providers/integrations/vercel/vercel-quota.ts +96 -0
- package/src/providers/integrations/xai/xai-oauth.ts +5 -5
- package/src/providers/integrations/zcode-start-plan/gifts.ts +1 -1
- package/src/providers/integrations/zcode-start-plan/oauth.ts +3 -3
- 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 +205 -0
- package/src/providers/operations/account-error-classification.ts +199 -307
- package/src/providers/operations/account-health-service.ts +82 -1
- package/src/providers/operations/account-refresh-status.ts +36 -37
- package/src/providers/operations/account-tier.ts +15 -16
- package/src/providers/operations/client-version-resolver.ts +1 -1
- package/src/providers/operations/client-versions.ts +100 -81
- package/src/providers/operations/cooldown-rules.ts +11 -16
- package/src/providers/operations/oauth-refresh-lead.ts +46 -22
- package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
- package/src/providers/operations/provider-catalog-service.ts +1 -1
- package/src/providers/operations/provider-credential-service.ts +118 -51
- 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/operations/session-resolution.ts +17 -1
- package/src/providers/provider-contracts.ts +6 -4
- package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
- package/src/providers/provider-metadata.ts +70 -10
- package/src/providers/provider-probe.ts +15 -1
- package/src/providers/provider-registry.ts +42 -33
- package/src/providers/quota/quota-contracts.ts +2 -0
- 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 +100 -21
- package/src/security/api-key-auth-cache.ts +1 -1
- package/src/security/api-key-auth.ts +6 -2
- package/src/security/ip-boundary.ts +1 -1
- package/src/transport/dispatch/attempt-finalize.ts +15 -14
- package/src/transport/dispatch/attempt-loop.ts +73 -19
- package/src/transport/dispatch/generation.ts +2 -8
- package/src/transport/dispatch/handler-deps.ts +1 -1
- package/src/transport/dispatch/leases.ts +1 -1
- package/src/transport/dispatch/native-shared.ts +25 -0
- package/src/transport/dispatch/proxy-request.ts +8 -46
- package/src/transport/dispatch/responses-compact.ts +2 -8
- package/src/transport/dispatch/retry-policy.ts +1 -1
- package/src/transport/dispatch/streaming/finalize.ts +10 -7
- package/src/transport/dispatch/streaming/stream.ts +9 -37
- package/src/transport/dispatch/systemone.ts +2 -8
- package/src/transport/dispatch/token-count.ts +2 -6
- package/src/transport/dispatch/upstream.ts +63 -0
- package/src/transport/drain-endpoint.ts +31 -4
- package/src/transport/failure-policy.ts +16 -30
- 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 +393 -32
- package/src/transport/shutdown-notice.ts +1 -1
- 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 +7 -1
- 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 +22 -5
- package/src/transport/surface/responses/parse.ts +3 -11
- package/src/workers/buddy-growth-tasks.ts +8 -8
- package/src/workers/daily-checkin.ts +4 -4
- package/src/workers/quota-refresh-sweep.ts +1 -1
- package/src/workers/zcode-gift-claim.ts +1 -1
- package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
- package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js +0 -9
- package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
- package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
- package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
- package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
- package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
- package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
- package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
- package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
- package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
- package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
- package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
- package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
- package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
- package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
- package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
- package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
- package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
- package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
- package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
- package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
- package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
- package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
- package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
- package/dist/dashboard/assets/pagination-ecPvG_8g.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-Ckm1UXuC.js.gz +0 -0
- package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
- package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
- package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
- package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
- package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
- package/src/network/pool-health.ts +0 -210
- package/src/providers/bundled-discovery-providers.ts +0 -98
- package/src/providers/configured-openai-providers.ts +0 -120
- package/src/providers/integrations/buddy/buddy-checkin.ts +0 -521
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +0 -76
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -130
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +0 -234
- package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -496
- package/src/providers/integrations/buddy/buddy-identity.ts +0 -300
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +0 -341
- package/src/providers/integrations/buddy/buddy-quota-shared.ts +0 -417
- package/src/providers/integrations/buddy/buddy-server-tools.ts +0 -240
- package/src/providers/integrations/buddy/buddy-usage-shared.ts +0 -221
- package/src/providers/integrations/buddy/buddy-usage-store.ts +0 -181
- package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +0 -37
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +0 -185
- package/src/providers/integrations/buddy/codebuddy/oauth.ts +0 -47
- package/src/providers/integrations/buddy/codebuddy/provider.ts +0 -175
- package/src/providers/integrations/buddy/codebuddy/quota.ts +0 -154
- package/src/providers/integrations/qoder/chat-proto.ts +0 -328
- package/src/providers/integrations/qoder/qoder-grpc.ts +0 -871
- package/src/providers/integrations/qoder/qoder-ide.ts +0 -588
- 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-B7kIWFm5.js → app-C0gTXEoN.js} +0 -0
- /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-C0gTXEoN.js.gz} +0 -0
- /package/src/network/{http2-bidi.ts → bidi.ts} +0 -0
- /package/src/network/pool/{pool-scripts.ts → scripts.ts} +0 -0
- /package/src/network/pool/{pool-state.ts → state.ts} +0 -0
- /package/src/network/{backoff.ts → util/backoff.ts} +0 -0
- /package/src/network/{deduplication.ts → util/dedupe.ts} +0 -0
- /package/src/network/{ip-utils.ts → util/ip.ts} +0 -0
|
@@ -29,7 +29,7 @@ import { resolveImageSource, splitDataUrl } from "../../../protocol/codecs";
|
|
|
29
29
|
import { IMAGE_CLAMP_MAX_BYTES } from "../../../protocol/image-clamp";
|
|
30
30
|
import type { ProviderAdapter, ProviderDispatchContext, ProviderDispatchTarget } from "../../provider-contracts";
|
|
31
31
|
import { readCredentialSecret } from "../../provider-contracts";
|
|
32
|
-
import type { Http2BidiEndReason
|
|
32
|
+
import type { Http2BidiEndReason } from "../../../network/bidi";
|
|
33
33
|
import type { ProviderContinuationScope } from "../../provider-contracts";
|
|
34
34
|
import { CURSOR_PROVIDER_ID, RUN_IDLE_TTL_MS, isAllowedAgentHost } from "./cursor-shared";
|
|
35
35
|
import {
|
|
@@ -54,7 +54,22 @@ import {
|
|
|
54
54
|
type CursorImageInput,
|
|
55
55
|
type CursorToolDescriptor,
|
|
56
56
|
} from "./cursor-request";
|
|
57
|
-
import {
|
|
57
|
+
import { decodeServerMessages, isRetryableOpenFailure, openRetryDelayMs, openCursorRunWithFallback } from "./cursor-http2";
|
|
58
|
+
import type { CursorRunTransport } from "./cursor-http2";
|
|
59
|
+
import {
|
|
60
|
+
classifyCursorFailure,
|
|
61
|
+
createCheckpointState,
|
|
62
|
+
createRunCompletion,
|
|
63
|
+
cursorQuietError,
|
|
64
|
+
drainInFlightDispatches,
|
|
65
|
+
frameKindOf,
|
|
66
|
+
frameShapeOf,
|
|
67
|
+
reconcileToolCount,
|
|
68
|
+
wireTraceEnabled,
|
|
69
|
+
CURSOR_WIRE_TRACE_MAX,
|
|
70
|
+
type CursorCheckpointState,
|
|
71
|
+
type CursorFrameShape,
|
|
72
|
+
} from "./cursor-hardening";
|
|
58
73
|
import { cursorRunSessions } from "./cursor-session-manager";
|
|
59
74
|
import type { CursorContinuationScope, CursorRunSession } from "./cursor-session-manager";
|
|
60
75
|
import { fetchCursorRunOrigin } from "./cursor-catalog";
|
|
@@ -64,6 +79,7 @@ import { decodeValueToJson, decodeStructEntriesToJson } from "./protocol/struct"
|
|
|
64
79
|
import { encodeMessage } from "./protocol/messages";
|
|
65
80
|
import { executeNativeTool, resolveExecFrame } from "./cursor-exec";
|
|
66
81
|
import { fnv1a32 } from "../shared/hash";
|
|
82
|
+
import { TtlCache } from "../../../persistence/ttl-cache";
|
|
67
83
|
|
|
68
84
|
/**
|
|
69
85
|
* Client-heartbeat cadence for every Cursor Run.
|
|
@@ -75,6 +91,34 @@ import { fnv1a32 } from "../shared/hash";
|
|
|
75
91
|
*/
|
|
76
92
|
const CURSOR_HEARTBEAT_INTERVAL_MS = 5_000;
|
|
77
93
|
|
|
94
|
+
/** Bounded open retries for connection/deadline failures (donor policy). */
|
|
95
|
+
const CURSOR_MAX_OPEN_RETRIES = 5;
|
|
96
|
+
|
|
97
|
+
/** Heartbeats with no semantic frame before a Run is declared quiet. */
|
|
98
|
+
const CURSOR_QUIET_BEATS = 48; // 48 × 5s = 240s (donor quiet window)
|
|
99
|
+
|
|
100
|
+
/** Optional client-version override for transport tests and live probes. */
|
|
101
|
+
const clientVersionOverride = (() => {
|
|
102
|
+
const raw = process.env.CARTETHYIA_CURSOR_CLIENT_VERSION?.trim();
|
|
103
|
+
return raw === undefined || raw === "" ? undefined : raw;
|
|
104
|
+
})();
|
|
105
|
+
|
|
106
|
+
/** Abortable open-retry backoff. */
|
|
107
|
+
function sleepBeforeOpenRetry(ms: number, signal: AbortSignal): Promise<void> {
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const timer = setTimeout(resolve, ms);
|
|
110
|
+
timer.unref?.();
|
|
111
|
+
signal.addEventListener(
|
|
112
|
+
"abort",
|
|
113
|
+
() => {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
reject(new GatewayError("transport_closed", 499, "The request was cancelled (request {requestId}). Send it again whenever ready 🙃"));
|
|
116
|
+
},
|
|
117
|
+
{ once: true },
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
78
122
|
/**
|
|
79
123
|
* How long one account's model roster is reused for `parameters` lookups.
|
|
80
124
|
*
|
|
@@ -84,25 +128,25 @@ const CURSOR_HEARTBEAT_INTERVAL_MS = 5_000;
|
|
|
84
128
|
*/
|
|
85
129
|
const CURSOR_ROSTER_TTL_MS = 300_000;
|
|
86
130
|
/**
|
|
87
|
-
* credential →
|
|
131
|
+
* credential → roster for the parameter lookup.
|
|
88
132
|
*
|
|
89
133
|
* Keyed by an FNV-1a digest of the credential rather than the raw token so a
|
|
90
134
|
* long-lived map does not hold live credentials as keys. Bounded by
|
|
91
|
-
* `ROSTER_CACHE_MAX` with
|
|
92
|
-
*
|
|
135
|
+
* `ROSTER_CACHE_MAX` with LRU eviction: the TTL is only checked on read, so
|
|
136
|
+
* without a cap rotated tokens would grow the map without bound. Implemented
|
|
137
|
+
* by the canonical `TtlCache` in LRU mode — re-setting moves the entry to the
|
|
138
|
+
* newest position, and the cap evicts the least-recently-used entry.
|
|
93
139
|
*/
|
|
94
140
|
const ROSTER_CACHE_MAX = 1024;
|
|
95
|
-
const rosterCache = new
|
|
141
|
+
const rosterCache = new TtlCache<{ roster: readonly CursorModelInfo[] }>({
|
|
142
|
+
ttlMs: CURSOR_ROSTER_TTL_MS,
|
|
143
|
+
maxEntries: ROSTER_CACHE_MAX,
|
|
144
|
+
lru: true,
|
|
145
|
+
});
|
|
96
146
|
|
|
97
147
|
function rosterCacheSet(credential: string, roster: readonly CursorModelInfo[]): void {
|
|
98
148
|
const key = String(fnv1a32(credential));
|
|
99
|
-
rosterCache.
|
|
100
|
-
rosterCache.set(key, { roster, fetchedAt: Date.now() });
|
|
101
|
-
while (rosterCache.size > ROSTER_CACHE_MAX) {
|
|
102
|
-
const oldest = rosterCache.keys().next().value;
|
|
103
|
-
if (oldest === undefined) break;
|
|
104
|
-
rosterCache.delete(oldest);
|
|
105
|
-
}
|
|
149
|
+
rosterCache.set(key, { roster });
|
|
106
150
|
}
|
|
107
151
|
|
|
108
152
|
/**
|
|
@@ -112,7 +156,7 @@ function rosterCacheSet(credential: string, roster: readonly CursorModelInfo[]):
|
|
|
112
156
|
export const rosterCacheForTests = {
|
|
113
157
|
size: (): number => rosterCache.size,
|
|
114
158
|
set: rosterCacheSet,
|
|
115
|
-
get: (credential: string): { roster: readonly CursorModelInfo[]
|
|
159
|
+
get: (credential: string): { roster: readonly CursorModelInfo[] } | undefined =>
|
|
116
160
|
rosterCache.get(String(fnv1a32(credential))),
|
|
117
161
|
};
|
|
118
162
|
/** Client-heartbeat cadence the Cursor edge expects (donor `heartbeatMs`). */
|
|
@@ -362,11 +406,27 @@ function resultImages(part: Extract<ContentPart, { kind: "toolResult" }>): Resul
|
|
|
362
406
|
|
|
363
407
|
/** One Run's live state while this adapter is pumping it. */
|
|
364
408
|
interface RunHandle {
|
|
365
|
-
readonly
|
|
409
|
+
readonly transport: CursorRunTransport;
|
|
366
410
|
readonly write: (message: Uint8Array) => Promise<void>;
|
|
367
411
|
close(reason?: string): void;
|
|
368
412
|
}
|
|
369
413
|
|
|
414
|
+
/** The donor's per-Run stream state, kept across a Run's request legs. */
|
|
415
|
+
interface CursorRunFrameState {
|
|
416
|
+
readonly completion: ReturnType<typeof createRunCompletion>;
|
|
417
|
+
readonly checkpoint: CursorCheckpointState;
|
|
418
|
+
/** Frame kinds since the last checkpoint, for the "went quiet" error. */
|
|
419
|
+
kindsSinceCheckpoint: string[];
|
|
420
|
+
/** Heartbeats since the last semantic frame; a quiet Run must not hang. */
|
|
421
|
+
quietBeats: number;
|
|
422
|
+
/** `tool_call_completed` frames the edge reported for this step. */
|
|
423
|
+
serverToolCount: number;
|
|
424
|
+
/** The transport this Run was opened on. */
|
|
425
|
+
transport: "http2" | "http1";
|
|
426
|
+
/** Attempt number, for the structured terminal error. */
|
|
427
|
+
attempt: number;
|
|
428
|
+
}
|
|
429
|
+
|
|
370
430
|
/** One decoded server frame, ready for pump consumption. */
|
|
371
431
|
type RunFrame = Record<string, unknown>;
|
|
372
432
|
|
|
@@ -539,26 +599,56 @@ class CursorAdapter implements ProviderAdapter {
|
|
|
539
599
|
);
|
|
540
600
|
}
|
|
541
601
|
const resolvedAll = pending.map((item) => cursorRunSessions.resolveCall(item.callId, scope));
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
602
|
+
const resolved = resolvedAll.filter((entry) => entry !== undefined);
|
|
603
|
+
const allSameRun =
|
|
604
|
+
resolved.length === pending.length && new Set(resolved.map((entry) => entry.session.runId)).size === 1;
|
|
605
|
+
// A call id that is live in this process but resolves to nothing under this
|
|
606
|
+
// caller's scope belongs to another tenant's Run. Rebasing over it would
|
|
607
|
+
// surface that conversation under this caller — always a hard refusal.
|
|
608
|
+
const foreignLive = pending.some(
|
|
609
|
+
(item) =>
|
|
610
|
+
cursorRunSessions.resolveCall(item.callId, scope) === undefined &&
|
|
611
|
+
cursorRunSessions.hasAnyCall(item.callId),
|
|
612
|
+
);
|
|
613
|
+
// Complete-replay rebase (donor / oh-my-pi behavior): when the request is
|
|
614
|
+
// the full transcript — every `call_cursor_*` result it carries is paired
|
|
615
|
+
// with a `toolCall` in the same conversation — and no foreign-live call is
|
|
616
|
+
// involved, cancel whatever live Runs its ids still touch and rebuild a
|
|
617
|
+
// fresh Run from history. This covers the cases the old code refused with
|
|
618
|
+
// a hard 400: a replay mixing results of an already-answered leg with a
|
|
619
|
+
// live one ("mixes live Cursor tool results"), and results spanning
|
|
620
|
+
// multiple live Runs. A hard refusal there is what stalled the client loop
|
|
621
|
+
// every time Cursor replays a superseded transcript; the donor instead
|
|
622
|
+
// closes the parked runs and answers with a fresh turn.
|
|
623
|
+
if (!allSameRun && !foreignLive && isCompleteToolHistoryReplay(request, pending)) {
|
|
624
|
+
for (const entry of resolved) {
|
|
625
|
+
cursorRunSessions.terminate(entry.session.runId, "Cursor Run superseded by a rebase");
|
|
626
|
+
this.#handles.delete(entry.session.runId);
|
|
627
|
+
this.cleanupReader(entry.session.runId);
|
|
628
|
+
}
|
|
556
629
|
yield* this.#startRun(request, candidate, context);
|
|
557
630
|
return;
|
|
558
631
|
}
|
|
559
|
-
const resolved = resolvedAll.filter((entry) => entry !== undefined);
|
|
560
632
|
const runId = resolved[0]?.session.runId;
|
|
561
|
-
if (
|
|
633
|
+
if (!allSameRun) {
|
|
634
|
+
// Not a safe rebase. Two distinct cases:
|
|
635
|
+
// 1. A partial tail — a result whose `toolCall` is absent from the sent
|
|
636
|
+
// conversation. That is not a complete replay, so there is nothing
|
|
637
|
+
// safe to rebase; name the missing ids so the client can resend the
|
|
638
|
+
// whole transcript.
|
|
639
|
+
// 2. A complete replay that still resolves to mixed runs — either a call
|
|
640
|
+
// id live under another scope (foreignLive, hard refusal) or results
|
|
641
|
+
// spanning more than one live Run with a transcript that is not a
|
|
642
|
+
// complete replay. Both stay refused rather than partially applied.
|
|
643
|
+
const missing = missingToolCallIds(request, pending);
|
|
644
|
+
if (missing.length > 0) {
|
|
645
|
+
throw new GatewayError(
|
|
646
|
+
"invalid_request",
|
|
647
|
+
400,
|
|
648
|
+
"This tool result has no matching tool call in the sent conversation (request {requestId}); resend the complete conversation. The session went cold — reconnect or resend 🔌",
|
|
649
|
+
{ callIds: missing },
|
|
650
|
+
);
|
|
651
|
+
}
|
|
562
652
|
throw new GatewayError(
|
|
563
653
|
"invalid_request",
|
|
564
654
|
400,
|
|
@@ -566,12 +656,10 @@ class CursorAdapter implements ProviderAdapter {
|
|
|
566
656
|
{ callIds: pending.map((item) => item.callId) },
|
|
567
657
|
);
|
|
568
658
|
}
|
|
569
|
-
if (
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
"These tool results belong to different Cursor Runs (request {requestId}); resend them one Run at a time. The session went cold — reconnect or resend 🔌",
|
|
574
|
-
);
|
|
659
|
+
if (runId === undefined) {
|
|
660
|
+
// Unreachable: every path that leaves `allSameRun` false throws above or
|
|
661
|
+
// rebases. This keeps the non-null narrowing legible for the rest.
|
|
662
|
+
throw new GatewayError("invalid_request", 400, "Cursor Run is no longer live (request {requestId}). The session went cold — reconnect or resend 🔌");
|
|
575
663
|
}
|
|
576
664
|
const session = cursorRunSessions.get(runId);
|
|
577
665
|
if (session === undefined) {
|
|
@@ -589,9 +677,10 @@ class CursorAdapter implements ProviderAdapter {
|
|
|
589
677
|
let sequence = 0;
|
|
590
678
|
const next = (): number => (sequence += 1);
|
|
591
679
|
try {
|
|
592
|
-
|
|
593
|
-
const
|
|
594
|
-
|
|
680
|
+
try {
|
|
681
|
+
for (const item of pending) {
|
|
682
|
+
const entry = cursorRunSessions.resolveCall(item.callId, scope);
|
|
683
|
+
if (entry === undefined) continue;
|
|
595
684
|
await handle.write(
|
|
596
685
|
buildMcpResult({
|
|
597
686
|
execId: entry.call.execId,
|
|
@@ -601,7 +690,25 @@ class CursorAdapter implements ProviderAdapter {
|
|
|
601
690
|
...(item.images.length > 0 ? { images: item.images } : {}),
|
|
602
691
|
}),
|
|
603
692
|
);
|
|
604
|
-
|
|
693
|
+
// Cleared only after the write succeeded: a half-delivered result
|
|
694
|
+
// must stay pending so a retry answers the same call, never a new one.
|
|
695
|
+
cursorRunSessions.clearPendingCall(runId, item.callId);
|
|
696
|
+
}
|
|
697
|
+
} catch (error) {
|
|
698
|
+
// The Run's transport rejected a result that the model is waiting on.
|
|
699
|
+
// The call was never delivered: terminate the held Run and rebase the
|
|
700
|
+
// whole transcript into a fresh Run (donor rule — a lost owner rebuilds
|
|
701
|
+
// Chat history on a fresh Run; a result is never replayed into an
|
|
702
|
+
// uncertain transport twice). Failing the request here instead left the
|
|
703
|
+
// client loop stuck: every resend hit the same dead handle.
|
|
704
|
+
cursorRunSessions.terminate(
|
|
705
|
+
runId,
|
|
706
|
+
`Cursor Run rejected a tool result: ${error instanceof Error ? error.message : String(error)}`,
|
|
707
|
+
);
|
|
708
|
+
this.#handles.delete(runId);
|
|
709
|
+
this.cleanupReader(runId);
|
|
710
|
+
yield* this.#startRun(request, candidate, context);
|
|
711
|
+
return;
|
|
605
712
|
}
|
|
606
713
|
yield* this.#pump({
|
|
607
714
|
request,
|
|
@@ -631,13 +738,23 @@ class CursorAdapter implements ProviderAdapter {
|
|
|
631
738
|
request: CanonicalRequest,
|
|
632
739
|
candidate: ProviderDispatchTarget,
|
|
633
740
|
context: ProviderDispatchContext,
|
|
741
|
+
retry?: {
|
|
742
|
+
readonly attempt: number;
|
|
743
|
+
readonly requestId: string;
|
|
744
|
+
readonly conversationId: string;
|
|
745
|
+
readonly checkpoint?: Uint8Array;
|
|
746
|
+
readonly forceH1?: boolean;
|
|
747
|
+
readonly rotateConversation?: boolean;
|
|
748
|
+
},
|
|
634
749
|
): AsyncIterable<CanonicalEvent> {
|
|
750
|
+
const attempt = retry?.attempt ?? 0;
|
|
635
751
|
const outboundHttp2 = context.outbound_http2;
|
|
636
|
-
|
|
752
|
+
const outboundFetch = context.outbound_fetch as unknown as typeof fetch | undefined;
|
|
753
|
+
if (outboundHttp2 === undefined && outboundFetch === undefined) {
|
|
637
754
|
throw new GatewayError(
|
|
638
755
|
"admission_unavailable",
|
|
639
756
|
503,
|
|
640
|
-
"Cursor requires validated
|
|
757
|
+
"Cursor requires validated egress for its Run transport (request {requestId}); none is configured for this route. No bridge to cross — configure a pool first 🏗️",
|
|
641
758
|
{ providerId: CURSOR_PROVIDER_ID },
|
|
642
759
|
);
|
|
643
760
|
}
|
|
@@ -661,28 +778,66 @@ class CursorAdapter implements ProviderAdapter {
|
|
|
661
778
|
const images = await collectImages(request, context);
|
|
662
779
|
const descriptors = toolDescriptorsFor(request.tools);
|
|
663
780
|
const systemPrompt = systemPromptFrom(request);
|
|
664
|
-
const conversationId = crypto.randomUUID();
|
|
781
|
+
const conversationId = retry?.conversationId ?? crypto.randomUUID();
|
|
665
782
|
const runId = crypto.randomUUID();
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
783
|
+
// Bounded open retries with the donor's backoff: connection and deadline
|
|
784
|
+
// failures retry; auth, quota, and request-shape refusals do not. Inside
|
|
785
|
+
// each attempt the transport policy applies (HTTP/2 first, HTTP/1.1 when
|
|
786
|
+
// HTTP/2 is blocked here), so a network that cannot carry HTTP/2 still
|
|
787
|
+
// serves Cursor without the caller ever seeing a transport error.
|
|
788
|
+
let opened: { transport: CursorRunTransport; requestId: string } | undefined;
|
|
789
|
+
let openError: unknown;
|
|
790
|
+
for (let openAttempt = 0; openAttempt <= CURSOR_MAX_OPEN_RETRIES; openAttempt += 1) {
|
|
791
|
+
if (context.abort_signal.aborted) {
|
|
792
|
+
throw new GatewayError("transport_closed", 499, "The request was cancelled (request {requestId}). Send it again whenever ready 🙃");
|
|
793
|
+
}
|
|
794
|
+
try {
|
|
795
|
+
opened = await openCursorRunWithFallback({
|
|
796
|
+
origin,
|
|
797
|
+
credential,
|
|
798
|
+
outboundHttp2,
|
|
799
|
+
outboundFetch,
|
|
800
|
+
signal: context.abort_signal,
|
|
801
|
+
...(retry?.requestId === undefined ? {} : { requestId: retry.requestId }),
|
|
802
|
+
...(retry?.forceH1 === true ? { forceH1: true } : {}),
|
|
803
|
+
...(clientVersionOverride === undefined ? {} : { clientVersion: clientVersionOverride }),
|
|
804
|
+
});
|
|
805
|
+
break;
|
|
806
|
+
} catch (error) {
|
|
807
|
+
openError = error;
|
|
808
|
+
if (!isRetryableOpenFailure(error) || openAttempt === CURSOR_MAX_OPEN_RETRIES) throw error;
|
|
809
|
+
await sleepBeforeOpenRetry(openRetryDelayMs(openAttempt), context.abort_signal);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
if (opened === undefined) throw openError;
|
|
813
|
+
const transport = opened.transport;
|
|
672
814
|
let closed = false;
|
|
673
815
|
const handle: RunHandle = {
|
|
674
|
-
|
|
675
|
-
write:
|
|
816
|
+
transport,
|
|
817
|
+
write: (message) => transport.write(message),
|
|
676
818
|
close: (reason) => {
|
|
677
819
|
if (closed) return;
|
|
678
820
|
closed = true;
|
|
679
821
|
if (reason !== undefined && reason.length > 0) {
|
|
680
|
-
void
|
|
822
|
+
void transport.write(buildCancelAction(reason)).catch(() => undefined);
|
|
681
823
|
}
|
|
682
|
-
|
|
824
|
+
transport.abort();
|
|
683
825
|
},
|
|
684
826
|
};
|
|
685
827
|
this.#handles.set(runId, handle);
|
|
828
|
+
this.#frameState.set(runId, {
|
|
829
|
+
completion: createRunCompletion(),
|
|
830
|
+
checkpoint: createCheckpointState(),
|
|
831
|
+
kindsSinceCheckpoint: [],
|
|
832
|
+
quietBeats: 0,
|
|
833
|
+
serverToolCount: 0,
|
|
834
|
+
transport: transport.mode,
|
|
835
|
+
attempt,
|
|
836
|
+
});
|
|
837
|
+
if (retry?.checkpoint !== undefined) {
|
|
838
|
+
const state = this.#frameState.get(runId);
|
|
839
|
+
if (state !== undefined) state.checkpoint.bytes = retry.checkpoint;
|
|
840
|
+
}
|
|
686
841
|
// Reader before the run request: frames can only arrive after it, but the
|
|
687
842
|
// reader must own them from the first byte, across every serving request.
|
|
688
843
|
this.#startReader(runId, handle);
|
|
@@ -699,12 +854,11 @@ class CursorAdapter implements ProviderAdapter {
|
|
|
699
854
|
let sequence = 0;
|
|
700
855
|
const next = (): number => (sequence += 1);
|
|
701
856
|
try {
|
|
702
|
-
await
|
|
857
|
+
await handle.write(
|
|
703
858
|
buildRunRequest({
|
|
704
859
|
text: liveUserText(request),
|
|
705
860
|
modelId,
|
|
706
861
|
conversationId,
|
|
707
|
-
runId,
|
|
708
862
|
...(images.length > 0 ? { images } : {}),
|
|
709
863
|
...(descriptors.length > 0 ? { descriptors } : {}),
|
|
710
864
|
...(systemPrompt === undefined ? {} : { systemPrompt }),
|
|
@@ -712,7 +866,10 @@ class CursorAdapter implements ProviderAdapter {
|
|
|
712
866
|
// A fresh Run always rebases the client's history into its user text:
|
|
713
867
|
// Cursor's own checkpoint cannot be reconstructed after a restart, and
|
|
714
868
|
// `root_prompt_messages_json` would suppress the RequestContext rules.
|
|
869
|
+
// A retry with a checkpoint resumes instead of replaying the turn.
|
|
715
870
|
history: historyFromRequest(request),
|
|
871
|
+
...(retry?.checkpoint === undefined ? {} : { conversationState: retry.checkpoint }),
|
|
872
|
+
...(retry?.checkpoint === undefined ? {} : { action: "resume" as const }),
|
|
716
873
|
}),
|
|
717
874
|
);
|
|
718
875
|
// Registered before the first frame is pumped: a tool call can arrive on
|
|
@@ -734,9 +891,54 @@ class CursorAdapter implements ProviderAdapter {
|
|
|
734
891
|
continuation: false,
|
|
735
892
|
});
|
|
736
893
|
} catch (error) {
|
|
737
|
-
|
|
894
|
+
const state = this.#frameState.get(runId);
|
|
895
|
+
const failure = error instanceof GatewayError
|
|
896
|
+
? error
|
|
897
|
+
: new GatewayError(
|
|
898
|
+
"platform_unavailable",
|
|
899
|
+
502,
|
|
900
|
+
`Cursor Run failed (request {requestId}): ${error instanceof Error ? error.message : String(error)}. The upstream came back empty-handed — retry 🌙`,
|
|
901
|
+
{ providerId: CURSOR_PROVIDER_ID },
|
|
902
|
+
"upstream",
|
|
903
|
+
);
|
|
904
|
+
const classification = classifyCursorFailure(
|
|
905
|
+
failure,
|
|
906
|
+
state?.transport ?? transport.mode,
|
|
907
|
+
);
|
|
908
|
+
// Only a Run that produced no observable event can be safely replayed:
|
|
909
|
+
// after the first content_delta/tool_call the client has already seen
|
|
910
|
+
// half an answer. This is the donor's replaySafe rule. A checkpoint
|
|
911
|
+
// carried across the retry stands for the work already done.
|
|
912
|
+
const replaySafe = sequence === 0;
|
|
913
|
+
if (
|
|
914
|
+
replaySafe &&
|
|
915
|
+
classification.retryable &&
|
|
916
|
+
(state?.attempt ?? 0) < CURSOR_MAX_OPEN_RETRIES &&
|
|
917
|
+
!context.abort_signal.aborted
|
|
918
|
+
) {
|
|
919
|
+
const nextAttempt = (state?.attempt ?? 0) + 1;
|
|
920
|
+
const checkpoint = state?.checkpoint.bytes;
|
|
921
|
+
await this.#drainDispatches(runId, context.abort_signal);
|
|
922
|
+
cursorRunSessions.terminate(runId, failure.message);
|
|
923
|
+
this.#handles.delete(runId);
|
|
924
|
+
this.cleanupReader(runId);
|
|
925
|
+
this.#frameState.delete(runId);
|
|
926
|
+
this.#dispatches.delete(runId);
|
|
927
|
+
this.#wireTraces.delete(runId);
|
|
928
|
+
await sleepBeforeOpenRetry(openRetryDelayMs(nextAttempt - 1), context.abort_signal);
|
|
929
|
+
yield* this.#startRun(request, candidate, context, {
|
|
930
|
+
attempt: nextAttempt,
|
|
931
|
+
requestId: opened.requestId,
|
|
932
|
+
conversationId: classification.rotateConversation ? crypto.randomUUID() : conversationId,
|
|
933
|
+
...(classification.fallbackH1 ? { forceH1: true } : {}),
|
|
934
|
+
...(checkpoint === undefined || classification.rotateConversation ? {} : { checkpoint }),
|
|
935
|
+
});
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
await this.#drainDispatches(runId, context.abort_signal);
|
|
939
|
+
cursorRunSessions.terminate(runId, failure.message);
|
|
738
940
|
this.#handles.delete(runId);
|
|
739
|
-
throw
|
|
941
|
+
throw structuredCursorFailure(failure, classification, state, runId);
|
|
740
942
|
} finally {
|
|
741
943
|
if (cursorRunSessions.get(runId) === undefined) {
|
|
742
944
|
this.#handles.delete(runId);
|
|
@@ -751,6 +953,9 @@ class CursorAdapter implements ProviderAdapter {
|
|
|
751
953
|
cursorRunSessions.terminate(runId);
|
|
752
954
|
this.#handles.delete(runId);
|
|
753
955
|
this.cleanupReader(runId);
|
|
956
|
+
this.#frameState.delete(runId);
|
|
957
|
+
this.#dispatches.delete(runId);
|
|
958
|
+
this.#wireTraces.delete(runId);
|
|
754
959
|
}
|
|
755
960
|
}
|
|
756
961
|
}
|
|
@@ -759,6 +964,35 @@ class CursorAdapter implements ProviderAdapter {
|
|
|
759
964
|
readonly #handles = new Map<string, RunHandle>();
|
|
760
965
|
/** One shared frame reader per live Run, surviving its requests. */
|
|
761
966
|
readonly #readers = new Map<string, RunFrameStream>();
|
|
967
|
+
/** Checkpoint/exec accounting per Run: survives every pump leg. */
|
|
968
|
+
readonly #frameState = new Map<string, CursorRunFrameState>();
|
|
969
|
+
/** Async exec handlers decoded from a chunk, drained on both pump exits. */
|
|
970
|
+
readonly #dispatches = new Map<string, Set<Promise<void>>>();
|
|
971
|
+
/** Opt-in shape-only traces of failed Runs. */
|
|
972
|
+
readonly #wireTraces = new Map<string, CursorFrameShape[]>();
|
|
973
|
+
|
|
974
|
+
/** Records one frame's shape (opt-in, never payload text/ids). */
|
|
975
|
+
#trace(runId: string, message: RunFrame): void {
|
|
976
|
+
if (!wireTraceEnabled()) return;
|
|
977
|
+
const trace = this.#wireTraces.get(runId) ?? [];
|
|
978
|
+
if (trace.length < CURSOR_WIRE_TRACE_MAX) trace.push(frameShapeOf(message));
|
|
979
|
+
this.#wireTraces.set(runId, trace);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/** Tracks one async exec handler; it removes itself when it settles. */
|
|
983
|
+
#trackDispatch(runId: string, dispatch: Promise<void>): void {
|
|
984
|
+
const dispatches = this.#dispatches.get(runId) ?? new Set<Promise<void>>();
|
|
985
|
+
dispatches.add(dispatch);
|
|
986
|
+
void dispatch.finally(() => dispatches.delete(dispatch));
|
|
987
|
+
this.#dispatches.set(runId, dispatches);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/** Drains every still-running exec handler before a Run is finalized. */
|
|
991
|
+
async #drainDispatches(runId: string, signal?: AbortSignal): Promise<void> {
|
|
992
|
+
const dispatches = this.#dispatches.get(runId);
|
|
993
|
+
if (dispatches === undefined) return;
|
|
994
|
+
await drainInFlightDispatches(dispatches, signal);
|
|
995
|
+
}
|
|
762
996
|
|
|
763
997
|
/**
|
|
764
998
|
* Opens the Run's single frame reader.
|
|
@@ -776,11 +1010,16 @@ class CursorAdapter implements ProviderAdapter {
|
|
|
776
1010
|
this.#readers.set(runId, frames);
|
|
777
1011
|
// The client-heartbeat cadence belongs to the reader: the Cursor edge
|
|
778
1012
|
// expects a keepalive while the Run stays open, including the window
|
|
779
|
-
// between a tool call and the request that carries its result.
|
|
1013
|
+
// between a tool call and the request that carries its result. A write
|
|
1014
|
+
// that failed means the transport died; stop the cadence rather than
|
|
1015
|
+
// spinning on a dead stream until the idle TTL fires.
|
|
780
1016
|
let state = "open";
|
|
781
1017
|
const beat = (): void => {
|
|
782
1018
|
if (state !== "open") return;
|
|
783
|
-
void handle.write(buildHeartbeat()).catch(() =>
|
|
1019
|
+
void handle.write(buildHeartbeat()).catch(() => {
|
|
1020
|
+
state = "failed";
|
|
1021
|
+
clearInterval(heartbeatTimer);
|
|
1022
|
+
});
|
|
784
1023
|
};
|
|
785
1024
|
const heartbeatTimer = setInterval(
|
|
786
1025
|
beat,
|
|
@@ -794,16 +1033,35 @@ class CursorAdapter implements ProviderAdapter {
|
|
|
794
1033
|
// Stream failures do not always surface as a throw from the body iterator;
|
|
795
1034
|
// the terminal event is the authoritative signal (auth revoked, quota,
|
|
796
1035
|
// RST_STREAM). `local-close` is our own abort: the session manager already
|
|
797
|
-
// decided the outcome, so the pump just sees a clean end.
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1036
|
+
// decided the outcome, so the pump just sees a clean end. Normalized over
|
|
1037
|
+
// both transports: HTTP/1.1 reports the same terminal kinds.
|
|
1038
|
+
const unsubscribeTerminal = handle.transport.onTerminal((terminal) => {
|
|
1039
|
+
if (terminal.kind === "local-close") return;
|
|
1040
|
+
const endReason: Http2BidiEndReason =
|
|
1041
|
+
terminal.kind === "remote-error"
|
|
1042
|
+
? { kind: "remote-error", error: terminal.failure ?? new Error("Cursor Run transport failed") }
|
|
1043
|
+
: { kind: terminal.kind };
|
|
1044
|
+
// A clean close carries no failure of its own: the pump decides whether
|
|
1045
|
+
// a turn without `turn_ended` is complete (text/step/tool batch) or
|
|
1046
|
+
// quiet. Inventing "ended before turn_ended" here made every held tool
|
|
1047
|
+
// round fail before its call could be handed to the client.
|
|
1048
|
+
const terminalFailure =
|
|
1049
|
+
terminal.failure ??
|
|
1050
|
+
(terminal.kind === "remote-error"
|
|
1051
|
+
? new GatewayError(
|
|
1052
|
+
"platform_unavailable",
|
|
1053
|
+
502,
|
|
1054
|
+
"Cursor Run transport failed (request {requestId}). The upstream came back empty-handed — retry 🌙",
|
|
1055
|
+
{},
|
|
1056
|
+
"network",
|
|
1057
|
+
)
|
|
1058
|
+
: undefined);
|
|
1059
|
+
frames.end(endReason, terminalFailure);
|
|
802
1060
|
unsubscribeTerminal();
|
|
803
1061
|
});
|
|
804
1062
|
void (async () => {
|
|
805
1063
|
try {
|
|
806
|
-
for await (const message of decodeServerMessages(handle.
|
|
1064
|
+
for await (const message of decodeServerMessages(handle.transport.body)) {
|
|
807
1065
|
frames.push(message);
|
|
808
1066
|
}
|
|
809
1067
|
// The body ended without a recorded terminal: treat it as a clean
|
|
@@ -879,10 +1137,11 @@ class CursorAdapter implements ProviderAdapter {
|
|
|
879
1137
|
context: ProviderDispatchContext,
|
|
880
1138
|
): Promise<readonly CursorModelInfo[]> {
|
|
881
1139
|
const key = String(fnv1a32(credential));
|
|
1140
|
+
// Peek first: `get` deletes expired entries, but the stale roster must
|
|
1141
|
+
// survive as the fallback when the refetch fails (see the catch below).
|
|
1142
|
+
const stale = rosterCache.peek(key);
|
|
882
1143
|
const cached = rosterCache.get(key);
|
|
883
|
-
if (cached !== undefined
|
|
884
|
-
return cached.roster;
|
|
885
|
-
}
|
|
1144
|
+
if (cached !== undefined) return cached.roster;
|
|
886
1145
|
try {
|
|
887
1146
|
const roster = await fetchCursorModels({
|
|
888
1147
|
credential,
|
|
@@ -894,7 +1153,7 @@ class CursorAdapter implements ProviderAdapter {
|
|
|
894
1153
|
rosterCacheSet(credential, roster);
|
|
895
1154
|
return roster;
|
|
896
1155
|
} catch {
|
|
897
|
-
return
|
|
1156
|
+
return stale?.roster ?? [];
|
|
898
1157
|
}
|
|
899
1158
|
}
|
|
900
1159
|
|
|
@@ -913,54 +1172,150 @@ class CursorAdapter implements ProviderAdapter {
|
|
|
913
1172
|
let terminalUsage: UsageRecord | undefined;
|
|
914
1173
|
let stopReason: CanonicalStopReason = "stop";
|
|
915
1174
|
let failure: GatewayError | undefined;
|
|
1175
|
+
const state = this.#frameState.get(runId) ?? {
|
|
1176
|
+
completion: createRunCompletion(),
|
|
1177
|
+
checkpoint: createCheckpointState(),
|
|
1178
|
+
kindsSinceCheckpoint: [],
|
|
1179
|
+
quietBeats: 0,
|
|
1180
|
+
serverToolCount: 0,
|
|
1181
|
+
transport: handle.transport.mode,
|
|
1182
|
+
attempt: 0,
|
|
1183
|
+
};
|
|
1184
|
+
this.#frameState.set(runId, state);
|
|
1185
|
+
const pendingToolCalls: Array<{ callId: string; toolName: string; arguments: string }> = [];
|
|
916
1186
|
const frames = this.#readers.get(runId) ?? this.#startReader(runId, handle);
|
|
917
1187
|
try {
|
|
918
1188
|
const messages = frames.subscribe();
|
|
919
1189
|
for await (const item of messages) {
|
|
920
1190
|
if (isRunStreamEnd(item)) {
|
|
921
|
-
|
|
1191
|
+
// The reader can end while an exec handler decoded from its last
|
|
1192
|
+
// chunk is still registering a call: drain before deciding.
|
|
1193
|
+
await this.#drainDispatches(runId, context.abort_signal);
|
|
1194
|
+
if (pendingToolCalls.length > 0) {
|
|
1195
|
+
for (const pending of pendingToolCalls.splice(0)) {
|
|
1196
|
+
yield {
|
|
1197
|
+
type: "tool_call_delta",
|
|
1198
|
+
sequence_number: input.next(),
|
|
1199
|
+
call_id: pending.callId,
|
|
1200
|
+
name: pending.toolName,
|
|
1201
|
+
arguments_delta: pending.arguments,
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
sawToolCall = true;
|
|
1205
|
+
cursorRunSessions.touch(runId);
|
|
1206
|
+
yield {
|
|
1207
|
+
type: "terminal",
|
|
1208
|
+
sequence_number: input.next(),
|
|
1209
|
+
state: "complete",
|
|
1210
|
+
stop_reason: "tool_use" as const,
|
|
1211
|
+
usage_deferred: true,
|
|
1212
|
+
};
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
if (item.failure !== undefined && item.__streamEnd.kind !== "remote-clean-close") {
|
|
1216
|
+
failure = item.failure;
|
|
1217
|
+
}
|
|
1218
|
+
state.completion.settle(item.failure === undefined ? "end" : "error");
|
|
922
1219
|
break;
|
|
923
1220
|
}
|
|
924
1221
|
const message = item;
|
|
1222
|
+
this.#trace(runId, message);
|
|
925
1223
|
if (context.abort_signal.aborted) {
|
|
926
1224
|
failure = new GatewayError("transport_closed", 499, "The request was cancelled (request {requestId}). Send it again whenever ready 🙃");
|
|
1225
|
+
state.completion.settle("timeout");
|
|
927
1226
|
break;
|
|
928
1227
|
}
|
|
1228
|
+
// Checkpoint retention: bytes are opaque and are never decoded here.
|
|
1229
|
+
// A checkpoint with no semantic frame since the previous one is a
|
|
1230
|
+
// repeated one: the streak is what the "went quiet" error names.
|
|
1231
|
+
const checkpointBytes = message["conversation_checkpoint_update"];
|
|
1232
|
+
if (checkpointBytes instanceof Uint8Array && checkpointBytes.byteLength > 0) {
|
|
1233
|
+
state.checkpoint.bytes = new Uint8Array(checkpointBytes);
|
|
1234
|
+
state.checkpoint.index += 1;
|
|
1235
|
+
state.checkpoint.step += 1;
|
|
1236
|
+
state.checkpoint.streak = state.kindsSinceCheckpoint.length === 0 ? state.checkpoint.streak + 1 : 1;
|
|
1237
|
+
state.checkpoint.kinds = state.kindsSinceCheckpoint.slice(-8);
|
|
1238
|
+
state.kindsSinceCheckpoint = [];
|
|
1239
|
+
state.quietBeats = 0;
|
|
1240
|
+
continue;
|
|
1241
|
+
}
|
|
1242
|
+
const kind = frameKindOf(message);
|
|
1243
|
+
state.kindsSinceCheckpoint.push(kind);
|
|
1244
|
+
if (state.kindsSinceCheckpoint.length > 16) state.kindsSinceCheckpoint.shift();
|
|
1245
|
+
|
|
929
1246
|
const execServer = recordOf(message["exec_server_message"]);
|
|
930
1247
|
if (execServer !== undefined) {
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1248
|
+
// Exec handlers are async and may outlive the chunk that spawned
|
|
1249
|
+
// them. Track the dispatch so BOTH exits drain it before terminal.
|
|
1250
|
+
const dispatch = (async () => {
|
|
1251
|
+
const handled = await this.#handleExecServerMessage(execServer, session, handle);
|
|
1252
|
+
if (handled.kind !== "tool_call") return;
|
|
1253
|
+
// Only MCP arms are relayed to the client for execution (client-
|
|
1254
|
+
// declared tools). Native tools are answered in-band by the executor.
|
|
1255
|
+
const callId = `call_cursor_${runId}_${handled.execId}`;
|
|
1256
|
+
const scope = context.continuation_scope;
|
|
1257
|
+
if (scope === undefined) {
|
|
1258
|
+
// Without a scope, no later request could be authorized to answer
|
|
1259
|
+
// this call, so the Run must not be held open for one.
|
|
1260
|
+
await handle.write(buildMcpRejected(handled.execId, "tool call is not answerable", handled.execIdString));
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
if (state.checkpoint.completedCallIds.has(callId)) {
|
|
1264
|
+
// The server already counted this call (donor rule): answering it
|
|
1265
|
+
// twice is what made tool rounds fail with duplicate results.
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
if (!cursorRunSessions.registerPendingCall(runId, {
|
|
1269
|
+
callId,
|
|
1270
|
+
execId: handled.execId,
|
|
1271
|
+
...(handled.execIdString === undefined ? {} : { execIdString: handled.execIdString }),
|
|
1272
|
+
toolName: handled.toolName,
|
|
1273
|
+
})) {
|
|
1274
|
+
await handle.write(buildMcpRejected(handled.execId, "tool call is not answerable", handled.execIdString));
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
state.checkpoint.pendingCallIds.add(callId);
|
|
1278
|
+
state.checkpoint.dispatchedCount += 1;
|
|
1279
|
+
sawToolCall = true;
|
|
1280
|
+
// The yield happens on the pump's own loop, not inside this async
|
|
1281
|
+
// dispatch: the queue below carries the call out to the client.
|
|
1282
|
+
pendingToolCalls.push({
|
|
1283
|
+
callId,
|
|
1284
|
+
toolName: handled.toolName,
|
|
1285
|
+
arguments: handled.arguments,
|
|
1286
|
+
});
|
|
1287
|
+
})().catch((error) => {
|
|
1288
|
+
failure = error instanceof GatewayError
|
|
1289
|
+
? error
|
|
1290
|
+
: new GatewayError(
|
|
1291
|
+
"platform_unavailable",
|
|
1292
|
+
502,
|
|
1293
|
+
`Cursor exec handler failed (request {requestId}): ${error instanceof Error ? error.message : String(error)}`,
|
|
1294
|
+
{ providerId: CURSOR_PROVIDER_ID },
|
|
1295
|
+
"upstream",
|
|
1296
|
+
);
|
|
1297
|
+
});
|
|
1298
|
+
this.#trackDispatch(runId, dispatch);
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// Drain handlers before surfacing a tool call: a second exec decoded
|
|
1303
|
+
// from the same chunk may still be registering its call, and the
|
|
1304
|
+
// client's interim terminal must carry the whole batch (donor park).
|
|
1305
|
+
if (pendingToolCalls.length > 0) {
|
|
1306
|
+
await this.#drainDispatches(runId, context.abort_signal);
|
|
1307
|
+
for (const pending of pendingToolCalls.splice(0)) {
|
|
1308
|
+
yield {
|
|
1309
|
+
type: "tool_call_delta",
|
|
1310
|
+
sequence_number: input.next(),
|
|
1311
|
+
call_id: pending.callId,
|
|
1312
|
+
name: pending.toolName,
|
|
1313
|
+
arguments_delta: pending.arguments,
|
|
1314
|
+
};
|
|
951
1315
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
sequence_number: input.next(),
|
|
956
|
-
call_id: callId,
|
|
957
|
-
name: handled.toolName,
|
|
958
|
-
arguments_delta: handled.arguments,
|
|
959
|
-
};
|
|
960
|
-
// Interim terminal the moment the call is registered and answered.
|
|
961
|
-
// The Run stays open for the result leg; this request must not wait
|
|
962
|
-
// for `turn_ended`, which only arrives after the client answers —
|
|
963
|
-
// holding it open here is exactly the deadlock that broke tool calling.
|
|
1316
|
+
// Interim terminal the moment the calls are registered: the Run stays
|
|
1317
|
+
// open for the result leg; waiting for `turn_ended` here deadlocked
|
|
1318
|
+
// the original tool loop.
|
|
964
1319
|
cursorRunSessions.touch(runId);
|
|
965
1320
|
yield {
|
|
966
1321
|
type: "terminal",
|
|
@@ -1013,12 +1368,45 @@ class CursorAdapter implements ProviderAdapter {
|
|
|
1013
1368
|
}
|
|
1014
1369
|
continue;
|
|
1015
1370
|
}
|
|
1016
|
-
// `tool_call_started` / `tool_call_completed` are display notifications
|
|
1017
|
-
//
|
|
1018
|
-
//
|
|
1371
|
+
// `tool_call_started` / `tool_call_completed` are display notifications,
|
|
1372
|
+
// but their count is authoritative for the step boundary: a step whose
|
|
1373
|
+
// calls are all counted is done without waiting out its heartbeats
|
|
1374
|
+
// (magpie 0.2.35 / oh-my-pi StepCalls rule).
|
|
1375
|
+
const listed = recordOf(interaction["tool_requests_listed"]);
|
|
1376
|
+
if (listed !== undefined) {
|
|
1377
|
+
const count = typeof listed["call_count"] === "number" ? listed["call_count"] : 0;
|
|
1378
|
+
state.checkpoint.expectedCallCount = count;
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1381
|
+
const completed = recordOf(interaction["tool_call_completed"]);
|
|
1382
|
+
if (completed !== undefined) {
|
|
1383
|
+
state.serverToolCount += 1;
|
|
1384
|
+
const callId = typeof completed["call_id"] === "string" ? completed["call_id"] : "";
|
|
1385
|
+
if (callId.length > 0) state.checkpoint.completedCallIds.add(callId);
|
|
1386
|
+
const reconciled = reconcileToolCount(state.checkpoint, state.serverToolCount);
|
|
1387
|
+
if (reconciled.endStep) {
|
|
1388
|
+
state.checkpoint.step = 0;
|
|
1389
|
+
state.quietBeats = 0;
|
|
1390
|
+
}
|
|
1391
|
+
continue;
|
|
1392
|
+
}
|
|
1393
|
+
const heartbeat = recordOf(interaction["heartbeat"]);
|
|
1394
|
+
if (heartbeat !== undefined) {
|
|
1395
|
+
state.quietBeats += 1;
|
|
1396
|
+
// A Run that keeps heartbeating after a checkpoint with no semantic
|
|
1397
|
+
// frame is "quiet": name the checkpoint instead of hanging forever.
|
|
1398
|
+
if (state.quietBeats >= CURSOR_QUIET_BEATS) {
|
|
1399
|
+
failure = cursorQuietError(state.checkpoint);
|
|
1400
|
+
state.completion.settle("timeout");
|
|
1401
|
+
break;
|
|
1402
|
+
}
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
// `turn_ended` is authoritative for the turn's end and its usage.
|
|
1019
1406
|
const turnEnded = recordOf(interaction["turn_ended"]);
|
|
1020
1407
|
if (turnEnded !== undefined) {
|
|
1021
1408
|
terminalUsage = usageFromTurnEnded(turnEnded);
|
|
1409
|
+
state.completion.settle("end");
|
|
1022
1410
|
break;
|
|
1023
1411
|
}
|
|
1024
1412
|
}
|
|
@@ -1033,18 +1421,47 @@ class CursorAdapter implements ProviderAdapter {
|
|
|
1033
1421
|
{ providerId: CURSOR_PROVIDER_ID },
|
|
1034
1422
|
"upstream",
|
|
1035
1423
|
);
|
|
1424
|
+
state.completion.settle("error");
|
|
1425
|
+
}
|
|
1426
|
+
// BOTH exits drain: an exec handler decoded from the last chunk can still
|
|
1427
|
+
// be running when the transport fails (donor L1198-L1201).
|
|
1428
|
+
await this.#drainDispatches(runId, context.abort_signal);
|
|
1429
|
+
// A dispatch that settled during the drain may have queued a call. Hand it
|
|
1430
|
+
// to the client before any terminal event, so a call is never lost.
|
|
1431
|
+
if (pendingToolCalls.length > 0) {
|
|
1432
|
+
for (const pending of pendingToolCalls.splice(0)) {
|
|
1433
|
+
yield {
|
|
1434
|
+
type: "tool_call_delta",
|
|
1435
|
+
sequence_number: input.next(),
|
|
1436
|
+
call_id: pending.callId,
|
|
1437
|
+
name: pending.toolName,
|
|
1438
|
+
arguments_delta: pending.arguments,
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
sawToolCall = true;
|
|
1442
|
+
cursorRunSessions.touch(runId);
|
|
1443
|
+
yield {
|
|
1444
|
+
type: "terminal",
|
|
1445
|
+
sequence_number: input.next(),
|
|
1446
|
+
state: "complete",
|
|
1447
|
+
stop_reason: "tool_use" as const,
|
|
1448
|
+
usage_deferred: true,
|
|
1449
|
+
};
|
|
1450
|
+
return;
|
|
1036
1451
|
}
|
|
1037
1452
|
|
|
1038
1453
|
if (failure !== undefined) {
|
|
1039
|
-
|
|
1454
|
+
const classification = classifyCursorFailure(failure, state.transport);
|
|
1455
|
+
const structured = structuredCursorFailure(failure, classification, state, runId);
|
|
1456
|
+
cursorRunSessions.terminate(runId, structured.message);
|
|
1040
1457
|
yield {
|
|
1041
1458
|
type: "terminal",
|
|
1042
1459
|
sequence_number: input.next(),
|
|
1043
1460
|
state: "failed",
|
|
1044
1461
|
stop_reason: "error",
|
|
1045
|
-
// The refusal text, so the caller sees
|
|
1462
|
+
// The refusal text, so the caller sees the checkpoint/transport reason
|
|
1046
1463
|
// instead of a terminal with no reason attached.
|
|
1047
|
-
stop_details: { code:
|
|
1464
|
+
stop_details: { code: structured.code, status: structured.status, message: structured.message },
|
|
1048
1465
|
...(terminalUsage === undefined ? {} : { usage: terminalUsage }),
|
|
1049
1466
|
};
|
|
1050
1467
|
return;
|
|
@@ -1284,6 +1701,33 @@ class CursorAdapter implements ProviderAdapter {
|
|
|
1284
1701
|
}
|
|
1285
1702
|
}
|
|
1286
1703
|
|
|
1704
|
+
/** Attaches the donor's structured failure shape to a terminal GatewayError. */
|
|
1705
|
+
function structuredCursorFailure(
|
|
1706
|
+
failure: GatewayError,
|
|
1707
|
+
classification: ReturnType<typeof classifyCursorFailure>,
|
|
1708
|
+
state: CursorRunFrameState | undefined,
|
|
1709
|
+
runId: string,
|
|
1710
|
+
): GatewayError {
|
|
1711
|
+
const checkpoint = state?.checkpoint;
|
|
1712
|
+
const quiet = classification.kind === "quiet" ? cursorQuietError(checkpoint) : undefined;
|
|
1713
|
+
const detail = quiet?.message ?? failure.message;
|
|
1714
|
+
return new GatewayError(
|
|
1715
|
+
failure.code,
|
|
1716
|
+
failure.status,
|
|
1717
|
+
`Cursor ${classification.kind}: ${detail} (attempt ${(state?.attempt ?? 0) + 1}/${CURSOR_MAX_OPEN_RETRIES + 1}, ` +
|
|
1718
|
+
`transport ${state?.transport ?? "unknown"}, checkpoint ${checkpoint?.index ?? 0}, run ${runId})`,
|
|
1719
|
+
{
|
|
1720
|
+
...failure.details,
|
|
1721
|
+
providerId: CURSOR_PROVIDER_ID,
|
|
1722
|
+
cursorFailureKind: classification.kind,
|
|
1723
|
+
retryable: classification.retryable,
|
|
1724
|
+
checkpointIndex: checkpoint?.index ?? 0,
|
|
1725
|
+
checkpointStreak: checkpoint?.streak ?? 0,
|
|
1726
|
+
},
|
|
1727
|
+
failure.origin,
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1287
1731
|
/** RequestContext Cursor receives in reply to a context probe. */
|
|
1288
1732
|
function probeContextFor(descriptors: readonly CursorToolDescriptor[]): Record<string, unknown> {
|
|
1289
1733
|
return {
|
|
@@ -1363,6 +1807,13 @@ function readPendingToolResult(
|
|
|
1363
1807
|
* when it resends its recorded transcript — a gateway restart between legs or
|
|
1364
1808
|
* an evicted Run — and the only shape safe to rebase into a fresh Run, where
|
|
1365
1809
|
* the recorded exchange travels as history.
|
|
1810
|
+
*
|
|
1811
|
+
* The matching `toolCall` may live anywhere in the conversation, not only in
|
|
1812
|
+
* the trailing assistant turn: Claude Code resends the full transcript, which
|
|
1813
|
+
* contains several earlier `call_cursor_*` rounds. A scan that accepted only
|
|
1814
|
+
* one trailing assistant/tool pair refused those complete replays with
|
|
1815
|
+
* "no longer pending" even though every result was paired and none was live
|
|
1816
|
+
* (mirrors `isCompleteContinuationReplay` in the preparer).
|
|
1366
1817
|
*/
|
|
1367
1818
|
function isCompleteToolHistoryReplay(
|
|
1368
1819
|
request: CanonicalRequest,
|
|
@@ -1377,6 +1828,20 @@ function isCompleteToolHistoryReplay(
|
|
|
1377
1828
|
return pending.every((item) => calledIds.has(item.callId));
|
|
1378
1829
|
}
|
|
1379
1830
|
|
|
1831
|
+
/** The `call_cursor_*` result ids with no matching `toolCall` in the conversation. */
|
|
1832
|
+
function missingToolCallIds(
|
|
1833
|
+
request: CanonicalRequest,
|
|
1834
|
+
pending: readonly PendingToolResult[],
|
|
1835
|
+
): string[] {
|
|
1836
|
+
const calledIds = new Set<string>();
|
|
1837
|
+
for (const message of request.messages) {
|
|
1838
|
+
for (const part of message.content) {
|
|
1839
|
+
if (part.kind === "toolCall") calledIds.add(part.call_id);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
return pending.filter((item) => !calledIds.has(item.callId)).map((item) => item.callId);
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1380
1845
|
/** Reconstructs one tool result's text and images for the adapter's tests. */
|
|
1381
1846
|
export { decodeStructEntriesToJson, decodeValueToJson };
|
|
1382
1847
|
|
|
@@ -1394,4 +1859,3 @@ function resolveCursorWireModelId(rawModelId: string): string {
|
|
|
1394
1859
|
if (id === "auto") return "default";
|
|
1395
1860
|
return rawModelId;
|
|
1396
1861
|
}
|
|
1397
|
-
|