cartethyia 2.8.0 → 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.
Files changed (348) hide show
  1. package/.env.example +3 -3
  2. package/dist/dashboard/assets/{BannedPage-Cc0vKpMG.js → BannedPage-Dq530awD.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-Dq530awD.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-Bqj2aa1T.js → CliToolDetailPage-DZzaS-7S.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-DZzaS-7S.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-CXsaOAO2.js → CliToolsPage-DaZZ3cqP.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-DaZZ3cqP.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-BeLpJ4ma.js → CombosPage-BlC_xdkX.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-BlC_xdkX.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-DnV0uZ3m.js → ConsoleLogPage-D-rqeVtG.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-D-rqeVtG.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-CBxAjo63.js → CustomizationPage-7xT5yg2E.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-7xT5yg2E.js.gz +0 -0
  14. package/dist/dashboard/assets/{HealthEventsModal-B5yEE0k6.js → HealthEventsModal-Clq8E9cW.js} +1 -1
  15. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js.gz +0 -0
  16. package/dist/dashboard/assets/{InjectSystemPromptPanel-BWFLECmz.js → InjectSystemPromptPanel-6Teoq9Se.js} +1 -1
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-6Teoq9Se.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-CgoPc5B9.js → LoginPage-CZuI0MAR.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-CZuI0MAR.js.gz +0 -0
  20. package/dist/dashboard/assets/{ModelPicker-D9bFQern.js → ModelPicker-C1hNTlkr.js} +1 -1
  21. package/dist/dashboard/assets/ModelPicker-C1hNTlkr.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-B_p4tL8k.js → OverviewPage-0yfhSI1C.js} +1 -1
  23. package/dist/dashboard/assets/OverviewPage-0yfhSI1C.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js +8 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProviderIcon-D_0PiZtg.js → ProviderIcon-Ddhm1InL.js} +1 -1
  27. package/dist/dashboard/assets/ProviderIcon-Ddhm1InL.js.gz +0 -0
  28. package/dist/dashboard/assets/{ProvidersPage-CLI2bJTK.js → ProvidersPage-C3fYuu6i.js} +1 -1
  29. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js.gz +0 -0
  30. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js +9 -0
  31. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js.gz +0 -0
  32. package/dist/dashboard/assets/{QuotaPage-DA-a1iSN.js → QuotaPage-D1RV_2BX.js} +1 -1
  33. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-CctTga2C.js → SettingsPage-BzZAvYSX.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-BzZAvYSX.js.gz +0 -0
  36. package/dist/dashboard/assets/{SetupPage-febLU4r1.js → SetupPage--s-EfEIL.js} +1 -1
  37. package/dist/dashboard/assets/SetupPage--s-EfEIL.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-M6fvR5Wz.js → StudioPage-BjXFGSK4.js} +1 -1
  39. package/dist/dashboard/assets/StudioPage-BjXFGSK4.js.gz +0 -0
  40. package/dist/dashboard/assets/{UsagePage-nyeuRqz9.js → UsagePage-CP8F8BN7.js} +1 -1
  41. package/dist/dashboard/assets/UsagePage-CP8F8BN7.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BFOlvrJC.js → api-keys-2hc12Uz2.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-2hc12Uz2.js.gz +0 -0
  44. package/dist/dashboard/assets/app-DGXbNfAR.css +2 -0
  45. package/dist/dashboard/assets/app-DGXbNfAR.css.gz +0 -0
  46. package/dist/dashboard/assets/{assert-CqTLw5e7.js → assert-BMcj1xZE.js} +1 -1
  47. package/dist/dashboard/assets/assert-BMcj1xZE.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-C83EqLB0.js → badge-DkRUuVfz.js} +1 -1
  49. package/dist/dashboard/assets/badge-DkRUuVfz.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-D1xn7pIe.js → banner-CDjW841Q.js} +1 -1
  51. package/dist/dashboard/assets/banner-CDjW841Q.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-DU3Lq5Lm.js → card-Bto5UlL9.js} +1 -1
  53. package/dist/dashboard/assets/card-Bto5UlL9.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-Dw1U_drK.js → cli-tools-DaH6g9hL.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-DaH6g9hL.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-CEL5Sg_J.js → clipboard-button-BYKBCzjk.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-BYKBCzjk.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-BjFR5UHy.js → dialog-DL0Zftwv.js} +1 -1
  59. package/dist/dashboard/assets/dialog-DL0Zftwv.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-HvBW0AjI.js → drawer-Dpx_UXaS.js} +1 -1
  61. package/dist/dashboard/assets/drawer-Dpx_UXaS.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-CECVELyy.js → entry-BV7-1NoM.js} +1 -1
  63. package/dist/dashboard/assets/entry-BV7-1NoM.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-DYYofQ7W.js → entry-CGvdNU6r.js} +1 -1
  65. package/dist/dashboard/assets/entry-CGvdNU6r.js.gz +0 -0
  66. package/dist/dashboard/assets/{entry-BSu3gFlt.js → entry-DScKYvZj.js} +3 -3
  67. package/dist/dashboard/assets/entry-DScKYvZj.js.gz +0 -0
  68. package/dist/dashboard/assets/{formatters-DLW4i8zK.js → formatters-BS3DZlZj.js} +1 -1
  69. package/dist/dashboard/assets/formatters-BS3DZlZj.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-1-fCqe64.js → index-SQnvvTWX.js} +2 -2
  71. package/dist/dashboard/assets/index-SQnvvTWX.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-DGvU2oLK.js → inline-Dl-cuLK0.js} +1 -1
  73. package/dist/dashboard/assets/inline-Dl-cuLK0.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-iNnO1X2K.js → input-Bfx0EzRc.js} +1 -1
  75. package/dist/dashboard/assets/input-Bfx0EzRc.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-0q8UGypb.js → layout-BxhbQ_JS.js} +1 -1
  77. package/dist/dashboard/assets/layout-BxhbQ_JS.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-C0w2bQjJ.js → list-row-D49Y4ptn.js} +1 -1
  79. package/dist/dashboard/assets/list-row-D49Y4ptn.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-M5aJ_xCa.js → live-CbVTdX9V.js} +1 -1
  81. package/dist/dashboard/assets/live-CbVTdX9V.js.gz +0 -0
  82. package/dist/dashboard/assets/{network-BKeQGT3K.js → network-JGvDveof.js} +1 -1
  83. package/dist/dashboard/assets/network-JGvDveof.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-CivAk3da.js → numbers-Dw6kiew-.js} +1 -1
  85. package/dist/dashboard/assets/numbers-Dw6kiew-.js.gz +0 -0
  86. package/dist/dashboard/assets/{pagination-CpzvI5ep.js → pagination-iUoI8GeE.js} +1 -1
  87. package/dist/dashboard/assets/pagination-iUoI8GeE.js.gz +0 -0
  88. package/dist/dashboard/assets/providers/sensenova-intl.png +0 -0
  89. package/dist/dashboard/assets/providers/sensenova.png +0 -0
  90. package/dist/dashboard/assets/{routing-BFQX2VBc.js → routing-8jZr-3WE.js} +1 -1
  91. package/dist/dashboard/assets/routing-8jZr-3WE.js.gz +0 -0
  92. package/dist/dashboard/assets/{select-CXbck0jG.js → select-oO1FxMVb.js} +1 -1
  93. package/dist/dashboard/assets/select-oO1FxMVb.js.gz +0 -0
  94. package/dist/dashboard/assets/{settings-C_XvMHh9.js → settings-CLr3kdIx.js} +1 -1
  95. package/dist/dashboard/assets/settings-CLr3kdIx.js.gz +0 -0
  96. package/dist/dashboard/assets/{stack-DggRiCxE.js → stack-Y3pDteZr.js} +1 -1
  97. package/dist/dashboard/assets/stack-Y3pDteZr.js.gz +0 -0
  98. package/dist/dashboard/assets/{state-ClFzfbYp.js → state-CArX_Iak.js} +1 -1
  99. package/dist/dashboard/assets/state-CArX_Iak.js.gz +0 -0
  100. package/dist/dashboard/assets/{switch-CTIFxKqS.js → switch-DwlwmDO-.js} +1 -1
  101. package/dist/dashboard/assets/switch-DwlwmDO-.js.gz +0 -0
  102. package/dist/dashboard/assets/{useMutation-BClG3yWj.js → useMutation-DJ9GYEgf.js} +1 -1
  103. package/dist/dashboard/assets/useMutation-DJ9GYEgf.js.gz +0 -0
  104. package/dist/dashboard/index.html +2 -2
  105. package/dist/dashboard/index.html.gz +0 -0
  106. package/dist/dashboard/precompressed-assets.json +51 -51
  107. package/migrations/full-postgres/0047_add_provider_device_flow_proxy.sql +17 -0
  108. package/migrations/lite-sqlite3/0047_add_provider_device_flow_proxy.sql +19 -0
  109. package/package.json +1 -1
  110. package/src/boot/app.ts +1 -1
  111. package/src/boot/dependencies.ts +4 -6
  112. package/src/config/providers.ts +6 -5
  113. package/src/config/spec.ts +8 -8
  114. package/src/console/backup/nine-router.ts +71 -10
  115. package/src/console/backup/service.ts +8 -1
  116. package/src/console/console-router.ts +1 -1
  117. package/src/console/domain-registration.ts +7 -2
  118. package/src/console/inferhub/client.ts +1 -1
  119. package/src/console/observability/live.ts +2 -2
  120. package/src/console/providers/catalog/contracts.ts +9 -3
  121. package/src/console/providers/catalog/model-operations.ts +2 -3
  122. package/src/console/providers/catalog/provider-operations.ts +1 -1
  123. package/src/console/providers/catalog/routes.ts +2 -2
  124. package/src/console/providers/catalog/store.ts +1914 -1907
  125. package/src/console/providers/detail/routes.ts +1 -0
  126. package/src/console/providers/detail/store.ts +4 -0
  127. package/src/console/providers/oauth/callback-listener.ts +30 -11
  128. package/src/console/providers/oauth/device-egress.ts +123 -0
  129. package/src/console/providers/oauth/routes.ts +123 -16
  130. package/src/console/quota/account-quota-tenant.ts +5 -5
  131. package/src/console/quota/account-test-egress.ts +2 -2
  132. package/src/console/quota/daily-growth-job.ts +1 -1
  133. package/src/console/routing/pools/contracts.ts +1 -1
  134. package/src/console/routing/pools/operations.ts +1 -1
  135. package/src/console/routing/pools/probe-verdict.ts +2 -2
  136. package/src/console/routing/pools/relay-deploy.ts +1 -1
  137. package/src/console/routing/pools/store.ts +4 -4
  138. package/src/network/client.ts +67 -0
  139. package/src/network/egress/bidi-session.ts +322 -0
  140. package/src/network/egress/dial.ts +201 -0
  141. package/src/network/egress/factory.ts +191 -0
  142. package/src/network/{outbound-fetch.ts → egress/fetch.ts} +195 -29
  143. package/src/network/egress/tls-profile.ts +123 -0
  144. package/src/network/egress/ws.ts +175 -0
  145. package/src/network/{types.ts → pool/agent-config.ts} +1 -1
  146. package/src/network/pool/agent.ts +2 -2
  147. package/src/network/{pool-cooldown-store.ts → pool/cooldown-store.ts} +7 -7
  148. package/src/network/{pool-health-machine.ts → pool/health.ts} +205 -6
  149. package/src/network/pool/resolver.ts +10 -849
  150. package/src/network/pool/selector.ts +2 -2
  151. package/src/network/ssrf.ts +1 -1
  152. package/src/network/{retry.ts → util/retry.ts} +1 -1
  153. package/src/observability/telemetry-buffer.ts +1 -1
  154. package/src/persistence/schema.ts +9 -0
  155. package/src/persistence/ttl-cache.ts +1 -1
  156. package/src/protocol/codecs.ts +58 -1
  157. package/src/protocol/request/messages.ts +31 -18
  158. package/src/protocol/response/messages.ts +34 -7
  159. package/src/protocol/translation/quirks.ts +15 -0
  160. package/src/protocol/upstream/send-claude-messages.ts +2 -2
  161. package/src/providers/adapters/compatible-adapter.ts +5 -0
  162. package/src/providers/authentication/oauth-client.ts +40 -6
  163. package/src/providers/authentication/oauth-device-flow.ts +7 -3
  164. package/src/providers/authentication/oauth-flow-store.ts +82 -1
  165. package/src/providers/authentication/oauth-refresh-service.ts +766 -739
  166. package/src/providers/default-registry.ts +76 -0
  167. package/src/providers/discovery/discovery-types.ts +7 -0
  168. package/src/providers/discovery/probe-phases.ts +6 -2
  169. package/src/providers/discovery/probing-service.ts +7 -0
  170. package/src/providers/integrations/antigravity/antigravity-protocol.ts +11 -0
  171. package/src/providers/integrations/buddy/account.ts +969 -0
  172. package/src/providers/integrations/buddy/capabilities.ts +10 -10
  173. package/src/providers/integrations/buddy/{buddy-catalog-shared.ts → catalog.ts} +198 -136
  174. package/src/providers/integrations/buddy/{buddy-chat-shared.ts → chat.ts} +5 -5
  175. package/src/providers/integrations/buddy/codebuddy/index.ts +429 -0
  176. package/src/providers/integrations/buddy/codebuddy/shared.ts +4 -4
  177. package/src/providers/integrations/buddy/{buddy-identity.ts → headers.ts} +138 -140
  178. package/src/providers/integrations/buddy/lifecycle.ts +1018 -0
  179. package/src/providers/integrations/buddy/{buddy-media.ts → media.ts} +1 -1
  180. package/src/providers/integrations/cline/cline-oauth.ts +10 -5
  181. package/src/providers/integrations/cline/cline.ts +3 -4
  182. package/src/providers/integrations/clixad/clixad-oauth.ts +98 -0
  183. package/src/providers/integrations/clixad/clixad-quota.ts +132 -0
  184. package/src/providers/integrations/clixad/provider.ts +145 -0
  185. package/src/providers/integrations/codex/codex-oauth.ts +8 -5
  186. package/src/providers/integrations/cursor/cursor-auth.ts +1 -1
  187. package/src/providers/integrations/cursor/cursor-hardening.ts +279 -0
  188. package/src/providers/integrations/cursor/cursor-http2.ts +605 -6
  189. package/src/providers/integrations/cursor/cursor-shared.ts +1 -1
  190. package/src/providers/integrations/cursor/cursor.ts +485 -70
  191. package/src/providers/integrations/deepai/deepai-catalog.ts +149 -0
  192. package/src/providers/integrations/deepai/deepai-common.ts +145 -0
  193. package/src/providers/integrations/deepai/deepai-oauth.ts +129 -0
  194. package/src/providers/integrations/deepai/deepai-quota.ts +97 -0
  195. package/src/providers/integrations/deepai/provider.ts +42 -0
  196. package/src/providers/integrations/devin/devin-auth.ts +7 -0
  197. package/src/providers/integrations/factory/factory-oauth.ts +16 -7
  198. package/src/providers/integrations/freebuff/freebuff.ts +2 -2
  199. package/src/providers/integrations/freepi/freepi-catalog.ts +5 -0
  200. package/src/providers/integrations/freepi/freepi-discovery.ts +2 -2
  201. package/src/providers/integrations/freepi/freepi-headers.ts +2 -2
  202. package/src/providers/integrations/freepi/freepi-protocol.ts +11 -2
  203. package/src/providers/integrations/freepi/freepi-realm.ts +0 -3
  204. package/src/providers/integrations/freepi/freepi-session.ts +158 -0
  205. package/src/providers/integrations/freepi/freepi.ts +80 -54
  206. package/src/providers/integrations/github/github-discovery.ts +30 -13
  207. package/src/providers/integrations/github/github-oauth.ts +217 -44
  208. package/src/providers/integrations/github/github-quota.ts +22 -5
  209. package/src/providers/integrations/github/github.ts +40 -32
  210. package/src/providers/integrations/grok/grok-oauth.ts +4 -4
  211. package/src/providers/integrations/hermes/hermes-oauth.ts +3 -3
  212. package/src/providers/integrations/hermes/hermes.ts +34 -5
  213. package/src/providers/integrations/kilo/kilo-oauth.ts +4 -4
  214. package/src/providers/integrations/kilo/kilo.ts +1 -1
  215. package/src/providers/integrations/kimi/kimi-oauth.ts +3 -3
  216. package/src/providers/integrations/kiro/kiro-oauth.ts +2 -2
  217. package/src/providers/integrations/muse/muse-oauth.ts +5 -5
  218. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  219. package/src/providers/integrations/openrouter/provider.ts +1 -1
  220. package/src/providers/integrations/perch/perch-protocol.ts +0 -3
  221. package/src/providers/integrations/perch/perch.ts +3 -3
  222. package/src/providers/integrations/qoder/claim-qoder-credits.ts +1 -1
  223. package/src/providers/integrations/qoder/qoder-adapter.ts +19 -113
  224. package/src/providers/integrations/qoder/qoder-auth.ts +8 -1
  225. package/src/providers/integrations/qoder/qoder-catalog.ts +1 -1
  226. package/src/providers/integrations/qoder/qoder-chat.ts +25 -8
  227. package/src/providers/integrations/qoder/qoder-checkin.ts +1 -1
  228. package/src/providers/integrations/qoder/qoder-cosy.ts +37 -9
  229. package/src/providers/integrations/qoder/qoder-oauth.ts +9 -2
  230. package/src/providers/integrations/qoder/qoder-realm.ts +13 -36
  231. package/src/providers/integrations/qoder/qoder-sse.ts +447 -68
  232. package/src/providers/integrations/qoder/qoder-types.ts +9 -0
  233. package/src/providers/integrations/qoder/qoder.ts +3 -25
  234. package/src/providers/integrations/sensenova/provider.ts +144 -0
  235. package/src/providers/integrations/trae/trae-quota.ts +15 -1
  236. package/src/providers/integrations/vercel/provider.ts +117 -0
  237. package/src/providers/integrations/vercel/vercel-quota.ts +96 -0
  238. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  239. package/src/providers/integrations/zcode-start-plan/gifts.ts +1 -1
  240. package/src/providers/integrations/zcode-start-plan/oauth.ts +3 -3
  241. package/src/providers/integrations/zro/zro.ts +2 -1
  242. package/src/providers/operations/account-refresh-status.ts +14 -0
  243. package/src/providers/operations/client-versions.ts +95 -30
  244. package/src/providers/operations/oauth-refresh-lead.ts +46 -22
  245. package/src/providers/operations/provider-caches.ts +1 -1
  246. package/src/providers/operations/provider-credential-service.ts +22 -25
  247. package/src/providers/operations/session-resolution.ts +17 -1
  248. package/src/providers/provider-contracts.ts +2 -2
  249. package/src/providers/provider-metadata.ts +30 -0
  250. package/src/providers/quota/quota-contracts.ts +2 -0
  251. package/src/providers/quota/refresh.ts +25 -3
  252. package/src/security/ip-boundary.ts +1 -1
  253. package/src/transport/dispatch/attempt-finalize.ts +1 -1
  254. package/src/transport/dispatch/attempt-loop.ts +3 -3
  255. package/src/transport/dispatch/handler-deps.ts +1 -1
  256. package/src/transport/dispatch/leases.ts +1 -1
  257. package/src/transport/dispatch/retry-policy.ts +1 -1
  258. package/src/transport/dispatch/streaming/finalize.ts +2 -2
  259. package/src/transport/dispatch/streaming/stream.ts +1 -1
  260. package/src/transport/dispatch/upstream.ts +1 -1
  261. package/src/transport/shutdown-notice.ts +1 -1
  262. package/src/workers/buddy-growth-tasks.ts +8 -8
  263. package/src/workers/daily-checkin.ts +4 -4
  264. package/src/workers/quota-refresh-sweep.ts +1 -1
  265. package/src/workers/zcode-gift-claim.ts +1 -1
  266. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
  267. package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
  268. package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
  269. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
  270. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
  271. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
  272. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
  273. package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
  274. package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
  275. package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
  276. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
  277. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +0 -8
  278. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
  279. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
  280. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
  281. package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js +0 -9
  282. package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
  283. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
  284. package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
  285. package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
  286. package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
  287. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
  288. package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
  289. package/dist/dashboard/assets/app-Dx2QgsNj.css +0 -2
  290. package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
  291. package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
  292. package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
  293. package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
  294. package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
  295. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
  296. package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
  297. package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
  298. package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
  299. package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
  300. package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
  301. package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
  302. package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
  303. package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
  304. package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
  305. package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
  306. package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
  307. package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
  308. package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
  309. package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
  310. package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
  311. package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
  312. package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
  313. package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
  314. package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
  315. package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
  316. package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
  317. package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
  318. package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
  319. package/src/console/providers/catalog/discover-constants.ts +0 -6
  320. package/src/network/pool-health.ts +0 -198
  321. package/src/providers/integrations/buddy/buddy-checkin.ts +0 -521
  322. package/src/providers/integrations/buddy/buddy-cli-headers.ts +0 -76
  323. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -130
  324. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +0 -234
  325. package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -496
  326. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +0 -341
  327. package/src/providers/integrations/buddy/buddy-quota-shared.ts +0 -417
  328. package/src/providers/integrations/buddy/buddy-server-tools.ts +0 -240
  329. package/src/providers/integrations/buddy/buddy-usage-shared.ts +0 -221
  330. package/src/providers/integrations/buddy/buddy-usage-store.ts +0 -181
  331. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +0 -37
  332. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +0 -188
  333. package/src/providers/integrations/buddy/codebuddy/oauth.ts +0 -47
  334. package/src/providers/integrations/buddy/codebuddy/provider.ts +0 -179
  335. package/src/providers/integrations/buddy/codebuddy/quota.ts +0 -154
  336. package/src/providers/integrations/qoder/chat-proto.ts +0 -328
  337. package/src/providers/integrations/qoder/qoder-grpc.ts +0 -870
  338. package/src/providers/integrations/qoder/qoder-ide.ts +0 -481
  339. /package/dist/dashboard/assets/{app-DIXLjojg.js → app-C0gTXEoN.js} +0 -0
  340. /package/dist/dashboard/assets/{app-DIXLjojg.js.gz → app-C0gTXEoN.js.gz} +0 -0
  341. /package/src/network/{http2-bidi.ts → bidi.ts} +0 -0
  342. /package/src/network/pool/{byte-accounting.ts → accounting.ts} +0 -0
  343. /package/src/network/pool/{pool-scripts.ts → scripts.ts} +0 -0
  344. /package/src/network/pool/{pool-state.ts → state.ts} +0 -0
  345. /package/src/network/{backoff.ts → util/backoff.ts} +0 -0
  346. /package/src/network/{deduplication.ts → util/dedupe.ts} +0 -0
  347. /package/src/network/{ip-utils.ts → util/ip.ts} +0 -0
  348. /package/src/network/{rate-limit-verdict.ts → verdict.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, ValidatedHttp2BidiStream } from "../../../network/http2-bidi";
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 { cursorRunTerminationError, decodeServerMessages, openCursorRun } from "./cursor-http2";
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";
@@ -76,6 +91,34 @@ import { TtlCache } from "../../../persistence/ttl-cache";
76
91
  */
77
92
  const CURSOR_HEARTBEAT_INTERVAL_MS = 5_000;
78
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
+
79
122
  /**
80
123
  * How long one account's model roster is reused for `parameters` lookups.
81
124
  *
@@ -363,11 +406,27 @@ function resultImages(part: Extract<ContentPart, { kind: "toolResult" }>): Resul
363
406
 
364
407
  /** One Run's live state while this adapter is pumping it. */
365
408
  interface RunHandle {
366
- readonly stream: ValidatedHttp2BidiStream;
409
+ readonly transport: CursorRunTransport;
367
410
  readonly write: (message: Uint8Array) => Promise<void>;
368
411
  close(reason?: string): void;
369
412
  }
370
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
+
371
430
  /** One decoded server frame, ready for pump consumption. */
372
431
  type RunFrame = Record<string, unknown>;
373
432
 
@@ -618,9 +677,10 @@ class CursorAdapter implements ProviderAdapter {
618
677
  let sequence = 0;
619
678
  const next = (): number => (sequence += 1);
620
679
  try {
621
- for (const item of pending) {
622
- const entry = cursorRunSessions.resolveCall(item.callId, scope);
623
- if (entry === undefined) continue;
680
+ try {
681
+ for (const item of pending) {
682
+ const entry = cursorRunSessions.resolveCall(item.callId, scope);
683
+ if (entry === undefined) continue;
624
684
  await handle.write(
625
685
  buildMcpResult({
626
686
  execId: entry.call.execId,
@@ -630,7 +690,25 @@ class CursorAdapter implements ProviderAdapter {
630
690
  ...(item.images.length > 0 ? { images: item.images } : {}),
631
691
  }),
632
692
  );
633
- cursorRunSessions.clearPendingCall(runId, item.callId);
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;
634
712
  }
635
713
  yield* this.#pump({
636
714
  request,
@@ -660,13 +738,23 @@ class CursorAdapter implements ProviderAdapter {
660
738
  request: CanonicalRequest,
661
739
  candidate: ProviderDispatchTarget,
662
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
+ },
663
749
  ): AsyncIterable<CanonicalEvent> {
750
+ const attempt = retry?.attempt ?? 0;
664
751
  const outboundHttp2 = context.outbound_http2;
665
- if (outboundHttp2 === undefined) {
752
+ const outboundFetch = context.outbound_fetch as unknown as typeof fetch | undefined;
753
+ if (outboundHttp2 === undefined && outboundFetch === undefined) {
666
754
  throw new GatewayError(
667
755
  "admission_unavailable",
668
756
  503,
669
- "Cursor requires validated HTTP/2 egress (request {requestId}); none is configured for this route. No bridge to cross — configure a pool first 🏗️",
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 🏗️",
670
758
  { providerId: CURSOR_PROVIDER_ID },
671
759
  );
672
760
  }
@@ -690,28 +778,66 @@ class CursorAdapter implements ProviderAdapter {
690
778
  const images = await collectImages(request, context);
691
779
  const descriptors = toolDescriptorsFor(request.tools);
692
780
  const systemPrompt = systemPromptFrom(request);
693
- const conversationId = crypto.randomUUID();
781
+ const conversationId = retry?.conversationId ?? crypto.randomUUID();
694
782
  const runId = crypto.randomUUID();
695
- const opened = await openCursorRun({
696
- origin,
697
- credential,
698
- outboundHttp2,
699
- signal: context.abort_signal,
700
- });
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;
701
814
  let closed = false;
702
815
  const handle: RunHandle = {
703
- stream: opened.stream,
704
- write: opened.write,
816
+ transport,
817
+ write: (message) => transport.write(message),
705
818
  close: (reason) => {
706
819
  if (closed) return;
707
820
  closed = true;
708
821
  if (reason !== undefined && reason.length > 0) {
709
- void opened.write(buildCancelAction(reason)).catch(() => undefined);
822
+ void transport.write(buildCancelAction(reason)).catch(() => undefined);
710
823
  }
711
- opened.stream.abort();
824
+ transport.abort();
712
825
  },
713
826
  };
714
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
+ }
715
841
  // Reader before the run request: frames can only arrive after it, but the
716
842
  // reader must own them from the first byte, across every serving request.
717
843
  this.#startReader(runId, handle);
@@ -728,7 +854,7 @@ class CursorAdapter implements ProviderAdapter {
728
854
  let sequence = 0;
729
855
  const next = (): number => (sequence += 1);
730
856
  try {
731
- await opened.write(
857
+ await handle.write(
732
858
  buildRunRequest({
733
859
  text: liveUserText(request),
734
860
  modelId,
@@ -740,7 +866,10 @@ class CursorAdapter implements ProviderAdapter {
740
866
  // A fresh Run always rebases the client's history into its user text:
741
867
  // Cursor's own checkpoint cannot be reconstructed after a restart, and
742
868
  // `root_prompt_messages_json` would suppress the RequestContext rules.
869
+ // A retry with a checkpoint resumes instead of replaying the turn.
743
870
  history: historyFromRequest(request),
871
+ ...(retry?.checkpoint === undefined ? {} : { conversationState: retry.checkpoint }),
872
+ ...(retry?.checkpoint === undefined ? {} : { action: "resume" as const }),
744
873
  }),
745
874
  );
746
875
  // Registered before the first frame is pumped: a tool call can arrive on
@@ -762,9 +891,54 @@ class CursorAdapter implements ProviderAdapter {
762
891
  continuation: false,
763
892
  });
764
893
  } catch (error) {
765
- cursorRunSessions.terminate(runId, error instanceof Error ? error.message : undefined);
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);
766
940
  this.#handles.delete(runId);
767
- throw error;
941
+ throw structuredCursorFailure(failure, classification, state, runId);
768
942
  } finally {
769
943
  if (cursorRunSessions.get(runId) === undefined) {
770
944
  this.#handles.delete(runId);
@@ -779,6 +953,9 @@ class CursorAdapter implements ProviderAdapter {
779
953
  cursorRunSessions.terminate(runId);
780
954
  this.#handles.delete(runId);
781
955
  this.cleanupReader(runId);
956
+ this.#frameState.delete(runId);
957
+ this.#dispatches.delete(runId);
958
+ this.#wireTraces.delete(runId);
782
959
  }
783
960
  }
784
961
  }
@@ -787,6 +964,35 @@ class CursorAdapter implements ProviderAdapter {
787
964
  readonly #handles = new Map<string, RunHandle>();
788
965
  /** One shared frame reader per live Run, surviving its requests. */
789
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
+ }
790
996
 
791
997
  /**
792
998
  * Opens the Run's single frame reader.
@@ -804,11 +1010,16 @@ class CursorAdapter implements ProviderAdapter {
804
1010
  this.#readers.set(runId, frames);
805
1011
  // The client-heartbeat cadence belongs to the reader: the Cursor edge
806
1012
  // expects a keepalive while the Run stays open, including the window
807
- // 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.
808
1016
  let state = "open";
809
1017
  const beat = (): void => {
810
1018
  if (state !== "open") return;
811
- void handle.write(buildHeartbeat()).catch(() => undefined);
1019
+ void handle.write(buildHeartbeat()).catch(() => {
1020
+ state = "failed";
1021
+ clearInterval(heartbeatTimer);
1022
+ });
812
1023
  };
813
1024
  const heartbeatTimer = setInterval(
814
1025
  beat,
@@ -822,16 +1033,35 @@ class CursorAdapter implements ProviderAdapter {
822
1033
  // Stream failures do not always surface as a throw from the body iterator;
823
1034
  // the terminal event is the authoritative signal (auth revoked, quota,
824
1035
  // RST_STREAM). `local-close` is our own abort: the session manager already
825
- // decided the outcome, so the pump just sees a clean end.
826
- const unsubscribeTerminal = handle.stream.onTerminal((reason) => {
827
- if (reason.kind === "local-close") return;
828
- const mapped = cursorRunTerminationError({ stream: handle.stream, reason });
829
- frames.end(reason, mapped === undefined ? undefined : mapped);
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);
830
1060
  unsubscribeTerminal();
831
1061
  });
832
1062
  void (async () => {
833
1063
  try {
834
- for await (const message of decodeServerMessages(handle.stream.body)) {
1064
+ for await (const message of decodeServerMessages(handle.transport.body)) {
835
1065
  frames.push(message);
836
1066
  }
837
1067
  // The body ended without a recorded terminal: treat it as a clean
@@ -942,54 +1172,150 @@ class CursorAdapter implements ProviderAdapter {
942
1172
  let terminalUsage: UsageRecord | undefined;
943
1173
  let stopReason: CanonicalStopReason = "stop";
944
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 }> = [];
945
1186
  const frames = this.#readers.get(runId) ?? this.#startReader(runId, handle);
946
1187
  try {
947
1188
  const messages = frames.subscribe();
948
1189
  for await (const item of messages) {
949
1190
  if (isRunStreamEnd(item)) {
950
- failure = item.failure;
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");
951
1219
  break;
952
1220
  }
953
1221
  const message = item;
1222
+ this.#trace(runId, message);
954
1223
  if (context.abort_signal.aborted) {
955
1224
  failure = new GatewayError("transport_closed", 499, "The request was cancelled (request {requestId}). Send it again whenever ready 🙃");
1225
+ state.completion.settle("timeout");
956
1226
  break;
957
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
+
958
1246
  const execServer = recordOf(message["exec_server_message"]);
959
1247
  if (execServer !== undefined) {
960
- const handled = await this.#handleExecServerMessage(execServer, session, handle);
961
- if (handled.kind !== "tool_call") continue;
962
- // Only MCP arms are relayed to the client for execution (client-
963
- // declared tools). Native tools are answered in-band by the executor.
964
- const callId = `call_cursor_${runId}_${handled.execId}`;
965
- const scope = context.continuation_scope;
966
- if (scope === undefined) {
967
- // Without a scope, no later request could be authorized to answer
968
- // this call, so the Run must not be held open for one.
969
- await handle.write(buildMcpRejected(handled.execId, "tool call is not answerable", handled.execIdString));
970
- continue;
971
- }
972
- if (!cursorRunSessions.registerPendingCall(runId, {
973
- callId,
974
- execId: handled.execId,
975
- ...(handled.execIdString === undefined ? {} : { execIdString: handled.execIdString }),
976
- toolName: handled.toolName,
977
- })) {
978
- await handle.write(buildMcpRejected(handled.execId, "tool call is not answerable", handled.execIdString));
979
- continue;
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
+ };
980
1315
  }
981
- sawToolCall = true;
982
- yield {
983
- type: "tool_call_delta",
984
- sequence_number: input.next(),
985
- call_id: callId,
986
- name: handled.toolName,
987
- arguments_delta: handled.arguments,
988
- };
989
- // Interim terminal the moment the call is registered and answered.
990
- // The Run stays open for the result leg; this request must not wait
991
- // for `turn_ended`, which only arrives after the client answers —
992
- // 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.
993
1319
  cursorRunSessions.touch(runId);
994
1320
  yield {
995
1321
  type: "terminal",
@@ -1042,12 +1368,45 @@ class CursorAdapter implements ProviderAdapter {
1042
1368
  }
1043
1369
  continue;
1044
1370
  }
1045
- // `tool_call_started` / `tool_call_completed` are display notifications:
1046
- // they report what Cursor's host did, not a call this gateway executes.
1047
- // Only `turn_ended` is authoritative for the turn's end and its usage.
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.
1048
1406
  const turnEnded = recordOf(interaction["turn_ended"]);
1049
1407
  if (turnEnded !== undefined) {
1050
1408
  terminalUsage = usageFromTurnEnded(turnEnded);
1409
+ state.completion.settle("end");
1051
1410
  break;
1052
1411
  }
1053
1412
  }
@@ -1062,18 +1421,47 @@ class CursorAdapter implements ProviderAdapter {
1062
1421
  { providerId: CURSOR_PROVIDER_ID },
1063
1422
  "upstream",
1064
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;
1065
1451
  }
1066
1452
 
1067
1453
  if (failure !== undefined) {
1068
- cursorRunSessions.terminate(runId, failure.message);
1454
+ const classification = classifyCursorFailure(failure, state.transport);
1455
+ const structured = structuredCursorFailure(failure, classification, state, runId);
1456
+ cursorRunSessions.terminate(runId, structured.message);
1069
1457
  yield {
1070
1458
  type: "terminal",
1071
1459
  sequence_number: input.next(),
1072
1460
  state: "failed",
1073
1461
  stop_reason: "error",
1074
- // The refusal text, so the caller sees "you hit your usage limit"
1462
+ // The refusal text, so the caller sees the checkpoint/transport reason
1075
1463
  // instead of a terminal with no reason attached.
1076
- stop_details: { code: failure.code, status: failure.status, message: failure.message },
1464
+ stop_details: { code: structured.code, status: structured.status, message: structured.message },
1077
1465
  ...(terminalUsage === undefined ? {} : { usage: terminalUsage }),
1078
1466
  };
1079
1467
  return;
@@ -1313,6 +1701,33 @@ class CursorAdapter implements ProviderAdapter {
1313
1701
  }
1314
1702
  }
1315
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
+
1316
1731
  /** RequestContext Cursor receives in reply to a context probe. */
1317
1732
  function probeContextFor(descriptors: readonly CursorToolDescriptor[]): Record<string, unknown> {
1318
1733
  return {