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.
Files changed (496) hide show
  1. package/.env.example +6 -5
  2. package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Dq530awD.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-Dq530awD.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.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-BmttNV3U.js → CliToolsPage-DaZZ3cqP.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-DaZZ3cqP.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-BE3qwwfC.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-CRZdmMIX.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-Dv_FJ6Er.js → CustomizationPage-7xT5yg2E.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-7xT5yg2E.js.gz +0 -0
  14. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js +1 -0
  15. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js.gz +0 -0
  16. package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-6Teoq9Se.js} +2 -2
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-6Teoq9Se.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CZuI0MAR.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-CZuI0MAR.js.gz +0 -0
  20. package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-C1hNTlkr.js} +1 -1
  21. package/dist/dashboard/assets/ModelPicker-C1hNTlkr.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-BE40s3u5.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-CQ6bPUf_.js → ProviderIcon-Ddhm1InL.js} +1 -1
  27. package/dist/dashboard/assets/ProviderIcon-Ddhm1InL.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js +1 -0
  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-D1RV_2BX.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-BzZAvYSX.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-BzZAvYSX.js.gz +0 -0
  36. package/dist/dashboard/assets/{SetupPage-CFNcATJJ.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-YoD4tY5l.js → StudioPage-BjXFGSK4.js} +1 -1
  39. package/dist/dashboard/assets/StudioPage-BjXFGSK4.js.gz +0 -0
  40. package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-CP8F8BN7.js} +1 -1
  41. package/dist/dashboard/assets/UsagePage-CP8F8BN7.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BTlkq_Uk.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-BMcj1xZE.js +1 -0
  47. package/dist/dashboard/assets/assert-BMcj1xZE.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-DkRUuVfz.js} +1 -1
  49. package/dist/dashboard/assets/badge-DkRUuVfz.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-CDjW841Q.js} +1 -1
  51. package/dist/dashboard/assets/banner-CDjW841Q.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-Bto5UlL9.js} +1 -1
  53. package/dist/dashboard/assets/card-Bto5UlL9.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-A1-v8-03.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-CJ4Zty89.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-B5L2jPQ_.js → dialog-DL0Zftwv.js} +1 -1
  59. package/dist/dashboard/assets/dialog-DL0Zftwv.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-DkjUouAj.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-B-LVLV6o.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-CAhdYl3B.js → entry-CGvdNU6r.js} +1 -1
  65. package/dist/dashboard/assets/entry-CGvdNU6r.js.gz +0 -0
  66. package/dist/dashboard/assets/entry-DScKYvZj.js +4 -0
  67. package/dist/dashboard/assets/entry-DScKYvZj.js.gz +0 -0
  68. package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-BS3DZlZj.js} +1 -1
  69. package/dist/dashboard/assets/formatters-BS3DZlZj.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-BU64Lqvp.js → index-SQnvvTWX.js} +2 -2
  71. package/dist/dashboard/assets/index-SQnvvTWX.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-B2aDLyyg.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-BCVkdR9d.js → input-Bfx0EzRc.js} +1 -1
  75. package/dist/dashboard/assets/input-Bfx0EzRc.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-DNcAdMK-.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-DgrIH0hw.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-Cae01iu8.js → live-CbVTdX9V.js} +1 -1
  81. package/dist/dashboard/assets/live-CbVTdX9V.js.gz +0 -0
  82. package/dist/dashboard/assets/{network-CRib7H_2.js → network-JGvDveof.js} +1 -1
  83. package/dist/dashboard/assets/network-JGvDveof.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-Dw6kiew-.js} +1 -1
  85. package/dist/dashboard/assets/numbers-Dw6kiew-.js.gz +0 -0
  86. package/dist/dashboard/assets/{pagination-ecPvG_8g.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/providers/zro.svg +3 -0
  91. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  92. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  93. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  94. package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-8jZr-3WE.js} +1 -1
  95. package/dist/dashboard/assets/routing-8jZr-3WE.js.gz +0 -0
  96. package/dist/dashboard/assets/{select-ChoFWyKz.js → select-oO1FxMVb.js} +2 -2
  97. package/dist/dashboard/assets/select-oO1FxMVb.js.gz +0 -0
  98. package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-CLr3kdIx.js} +1 -1
  99. package/dist/dashboard/assets/settings-CLr3kdIx.js.gz +0 -0
  100. package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-Y3pDteZr.js} +1 -1
  101. package/dist/dashboard/assets/stack-Y3pDteZr.js.gz +0 -0
  102. package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-CArX_Iak.js} +1 -1
  103. package/dist/dashboard/assets/state-CArX_Iak.js.gz +0 -0
  104. package/dist/dashboard/assets/{switch-Db70BMix.js → switch-DwlwmDO-.js} +1 -1
  105. package/dist/dashboard/assets/switch-DwlwmDO-.js.gz +0 -0
  106. package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-DJ9GYEgf.js} +1 -1
  107. package/dist/dashboard/assets/useMutation-DJ9GYEgf.js.gz +0 -0
  108. package/dist/dashboard/index.html +2 -2
  109. package/dist/dashboard/index.html.gz +0 -0
  110. package/dist/dashboard/precompressed-assets.json +54 -53
  111. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  112. package/migrations/full-postgres/0047_add_provider_device_flow_proxy.sql +17 -0
  113. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  114. package/migrations/lite-sqlite3/0047_add_provider_device_flow_proxy.sql +19 -0
  115. package/package.json +1 -1
  116. package/src/boot/app.ts +11 -14
  117. package/src/boot/dependencies.ts +5 -7
  118. package/src/boot/flush.ts +20 -0
  119. package/src/boot/lifecycle.ts +7 -5
  120. package/src/config/providers.ts +6 -5
  121. package/src/config/routing.ts +26 -0
  122. package/src/config/spec-metadata.ts +72 -0
  123. package/src/config/spec.ts +28 -8
  124. package/src/console/api-keys/contracts.ts +12 -1
  125. package/src/console/api-keys/routes.ts +40 -28
  126. package/src/console/auth/account-reset.ts +7 -9
  127. package/src/console/backup/contracts.ts +4 -0
  128. package/src/console/backup/export-endpoint.ts +7 -5
  129. package/src/console/backup/nine-router.ts +71 -10
  130. package/src/console/backup/service.ts +8 -1
  131. package/src/console/cli-tools/service.ts +6 -12
  132. package/src/console/console-router-guards.ts +23 -16
  133. package/src/console/console-router.ts +1 -1
  134. package/src/console/domain-registration.ts +7 -4
  135. package/src/console/inferhub/client.ts +1 -1
  136. package/src/console/inferhub/normalize.ts +24 -28
  137. package/src/console/inferhub/types.ts +3 -0
  138. package/src/console/ip-bans/routes.ts +7 -13
  139. package/src/console/observability/live.ts +2 -2
  140. package/src/console/observability/store.ts +0 -53
  141. package/src/console/providers/catalog/contracts.ts +9 -3
  142. package/src/console/providers/catalog/model-operations.ts +2 -3
  143. package/src/console/providers/catalog/provider-operations.ts +1 -1
  144. package/src/console/providers/catalog/routes.ts +3 -2
  145. package/src/console/providers/catalog/store.ts +1914 -1902
  146. package/src/console/providers/detail/routes.ts +1 -0
  147. package/src/console/providers/detail/store.ts +4 -0
  148. package/src/console/providers/oauth/callback-listener.ts +30 -11
  149. package/src/console/providers/oauth/device-egress.ts +123 -0
  150. package/src/console/providers/oauth/routes.ts +123 -16
  151. package/src/console/quota/account-quota-global.ts +14 -24
  152. package/src/console/quota/account-quota-shared.ts +49 -0
  153. package/src/console/quota/account-quota-tenant.ts +11 -23
  154. package/src/console/quota/account-test-egress.ts +2 -2
  155. package/src/console/quota/daily-growth-job.ts +1 -1
  156. package/src/console/quota/quota-view.ts +19 -3
  157. package/src/console/routing/pools/contracts.ts +1 -1
  158. package/src/console/routing/pools/operations.ts +32 -47
  159. package/src/console/routing/pools/probe-verdict.ts +2 -2
  160. package/src/console/routing/pools/relay-deploy.ts +1 -1
  161. package/src/console/routing/pools/store.ts +4 -4
  162. package/src/console/shared/errors.ts +33 -6
  163. package/src/main.ts +2 -3
  164. package/src/network/client.ts +67 -0
  165. package/src/network/egress/bidi-session.ts +322 -0
  166. package/src/network/egress/dial.ts +201 -0
  167. package/src/network/egress/factory.ts +191 -0
  168. package/src/network/{outbound-fetch.ts → egress/fetch.ts} +195 -29
  169. package/src/network/egress/tls-profile.ts +123 -0
  170. package/src/network/egress/ws.ts +175 -0
  171. package/src/network/pool/{byte-accounting.ts → accounting.ts} +3 -2
  172. package/src/network/{types.ts → pool/agent-config.ts} +1 -1
  173. package/src/network/pool/agent.ts +2 -2
  174. package/src/network/{pool-cooldown-store.ts → pool/cooldown-store.ts} +95 -20
  175. package/src/network/{pool-health-machine.ts → pool/health.ts} +205 -6
  176. package/src/network/pool/resolver.ts +10 -849
  177. package/src/network/pool/selector.ts +184 -41
  178. package/src/network/ssrf.ts +7 -7
  179. package/src/network/{retry.ts → util/retry.ts} +1 -1
  180. package/src/network/{rate-limit-verdict.ts → verdict.ts} +1 -1
  181. package/src/observability/event-log.ts +7 -361
  182. package/src/observability/performance-metrics.ts +9 -0
  183. package/src/observability/runtime-metrics.ts +14 -9
  184. package/src/observability/telemetry-buffer.ts +16 -26
  185. package/src/observability/telemetry-reconciliation.ts +9 -37
  186. package/src/observability/telemetry-row.ts +125 -0
  187. package/src/persistence/db-mode.ts +4 -1
  188. package/src/persistence/dialect.ts +18 -0
  189. package/src/persistence/migrate.ts +6 -3
  190. package/src/persistence/schema.ts +17 -0
  191. package/src/persistence/ttl-cache.ts +21 -3
  192. package/src/protocol/codecs.ts +59 -2
  193. package/src/protocol/dsml.ts +34 -86
  194. package/src/protocol/markup-stream-parser.ts +195 -0
  195. package/src/protocol/reasoning-text.ts +19 -0
  196. package/src/protocol/request/chat.ts +10 -10
  197. package/src/protocol/request/gemini.ts +8 -2
  198. package/src/protocol/request/messages.ts +34 -22
  199. package/src/protocol/response/chat.ts +12 -20
  200. package/src/protocol/response/messages.ts +34 -7
  201. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  202. package/src/protocol/response/responses.ts +18 -2
  203. package/src/protocol/stop-reason.ts +23 -1
  204. package/src/protocol/think-split.ts +112 -0
  205. package/src/protocol/translation/dialect/qwen.ts +26 -58
  206. package/src/protocol/translation/quirks.ts +15 -0
  207. package/src/protocol/upstream/send-claude-messages.ts +2 -2
  208. package/src/providers/adapters/compatible-adapter.ts +76 -12
  209. package/src/providers/authentication/oauth-client.ts +40 -6
  210. package/src/providers/authentication/oauth-device-flow.ts +105 -11
  211. package/src/providers/authentication/oauth-flow-store.ts +82 -1
  212. package/src/providers/authentication/oauth-refresh-service.ts +766 -758
  213. package/src/providers/configured-provider.ts +148 -0
  214. package/src/providers/default-registry.ts +137 -36
  215. package/src/providers/discovery/discovery-types.ts +7 -0
  216. package/src/providers/discovery/probe-phases.ts +20 -6
  217. package/src/providers/discovery/probing-service.ts +22 -2
  218. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  219. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  220. package/src/providers/integrations/antigravity/antigravity-protocol.ts +16 -1
  221. package/src/providers/integrations/buddy/account.ts +969 -0
  222. package/src/providers/integrations/buddy/capabilities.ts +12 -12
  223. package/src/providers/integrations/buddy/{buddy-catalog-shared.ts → catalog.ts} +198 -136
  224. package/src/providers/integrations/buddy/{buddy-chat-shared.ts → chat.ts} +176 -20
  225. package/src/providers/integrations/buddy/codebuddy/index.ts +429 -0
  226. package/src/providers/integrations/buddy/codebuddy/shared.ts +41 -26
  227. package/src/providers/integrations/buddy/headers.ts +311 -0
  228. package/src/providers/integrations/buddy/lifecycle.ts +1018 -0
  229. package/src/providers/integrations/buddy/{buddy-media.ts → media.ts} +1 -1
  230. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  231. package/src/providers/integrations/cline/cline-oauth.ts +10 -5
  232. package/src/providers/integrations/cline/cline.ts +5 -6
  233. package/src/providers/integrations/clixad/clixad-oauth.ts +98 -0
  234. package/src/providers/integrations/clixad/clixad-quota.ts +132 -0
  235. package/src/providers/integrations/clixad/provider.ts +145 -0
  236. package/src/providers/integrations/codex/codex-oauth.ts +8 -5
  237. package/src/providers/integrations/commandcode/quota.ts +22 -28
  238. package/src/providers/integrations/cursor/cursor-auth.ts +1 -9
  239. package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
  240. package/src/providers/integrations/cursor/cursor-hardening.ts +279 -0
  241. package/src/providers/integrations/cursor/cursor-http2.ts +605 -6
  242. package/src/providers/integrations/cursor/cursor-request.ts +26 -12
  243. package/src/providers/integrations/cursor/cursor-shared.ts +8 -5
  244. package/src/providers/integrations/cursor/cursor.ts +574 -110
  245. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  246. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  247. package/src/providers/integrations/deepai/deepai-catalog.ts +149 -0
  248. package/src/providers/integrations/deepai/deepai-common.ts +145 -0
  249. package/src/providers/integrations/deepai/deepai-oauth.ts +129 -0
  250. package/src/providers/integrations/deepai/deepai-quota.ts +97 -0
  251. package/src/providers/integrations/deepai/provider.ts +42 -0
  252. package/src/providers/integrations/devin/devin-auth.ts +7 -0
  253. package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
  254. package/src/providers/integrations/factory/factory-oauth.ts +33 -51
  255. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  256. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  257. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  258. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  259. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  260. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  261. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  262. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  263. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  264. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  265. package/src/providers/integrations/freepi/freepi-catalog.ts +5 -0
  266. package/src/providers/integrations/freepi/freepi-discovery.ts +2 -2
  267. package/src/providers/integrations/freepi/freepi-headers.ts +2 -2
  268. package/src/providers/integrations/freepi/freepi-protocol.ts +11 -2
  269. package/src/providers/integrations/freepi/freepi-realm.ts +0 -3
  270. package/src/providers/integrations/freepi/freepi-session.ts +158 -0
  271. package/src/providers/integrations/freepi/freepi.ts +80 -54
  272. package/src/providers/integrations/github/github-discovery.ts +30 -13
  273. package/src/providers/integrations/github/github-oauth.ts +217 -44
  274. package/src/providers/integrations/github/github-quota.ts +22 -5
  275. package/src/providers/integrations/github/github.ts +40 -32
  276. package/src/providers/integrations/grok/grok-oauth.ts +52 -49
  277. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  278. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  279. package/src/providers/integrations/hermes/hermes-oauth.ts +3 -3
  280. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +42 -13
  281. package/src/providers/integrations/hermes/index.ts +2 -0
  282. package/src/providers/integrations/kilo/kilo-oauth.ts +4 -4
  283. package/src/providers/integrations/kilo/kilo.ts +1 -1
  284. package/src/providers/integrations/kimi/kimi-oauth.ts +3 -3
  285. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  286. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  287. package/src/providers/integrations/kiro/kiro-oauth.ts +2 -2
  288. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  289. package/src/providers/integrations/kiro/kiro.ts +15 -19
  290. package/src/providers/integrations/muse/muse-oauth.ts +5 -5
  291. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  292. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  293. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  294. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  295. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  296. package/src/providers/integrations/openrouter/provider.ts +1 -1
  297. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  298. package/src/providers/integrations/perch/perch-protocol.ts +0 -6
  299. package/src/providers/integrations/perch/perch-quota.ts +2 -16
  300. package/src/providers/integrations/perch/perch.ts +6 -16
  301. package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
  302. package/src/providers/integrations/qoder/qoder-adapter.ts +20 -125
  303. package/src/providers/integrations/qoder/qoder-auth.ts +109 -30
  304. package/src/providers/integrations/qoder/qoder-catalog.ts +21 -6
  305. package/src/providers/integrations/qoder/qoder-chat.ts +32 -29
  306. package/src/providers/integrations/qoder/qoder-checkin.ts +5 -5
  307. package/src/providers/integrations/qoder/qoder-cosy.ts +37 -9
  308. package/src/providers/integrations/qoder/qoder-oauth.ts +9 -2
  309. package/src/providers/integrations/qoder/qoder-realm.ts +13 -36
  310. package/src/providers/integrations/qoder/qoder-sse.ts +447 -68
  311. package/src/providers/integrations/qoder/qoder-types.ts +9 -0
  312. package/src/providers/integrations/qoder/qoder.ts +3 -26
  313. package/src/providers/integrations/sensenova/provider.ts +144 -0
  314. package/src/providers/integrations/trae/trae-protocol.ts +9 -5
  315. package/src/providers/integrations/trae/trae-quota.ts +21 -29
  316. package/src/providers/integrations/trae/trae-realm.ts +38 -0
  317. package/src/providers/integrations/trae/trae.ts +7 -24
  318. package/src/providers/integrations/vercel/provider.ts +117 -0
  319. package/src/providers/integrations/vercel/vercel-quota.ts +96 -0
  320. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  321. package/src/providers/integrations/zcode-start-plan/gifts.ts +1 -1
  322. package/src/providers/integrations/zcode-start-plan/oauth.ts +3 -3
  323. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  324. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  325. package/src/providers/integrations/zro/zro.ts +205 -0
  326. package/src/providers/operations/account-error-classification.ts +199 -307
  327. package/src/providers/operations/account-health-service.ts +82 -1
  328. package/src/providers/operations/account-refresh-status.ts +36 -37
  329. package/src/providers/operations/account-tier.ts +15 -16
  330. package/src/providers/operations/client-version-resolver.ts +1 -1
  331. package/src/providers/operations/client-versions.ts +100 -81
  332. package/src/providers/operations/cooldown-rules.ts +11 -16
  333. package/src/providers/operations/oauth-refresh-lead.ts +46 -22
  334. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  335. package/src/providers/operations/provider-catalog-service.ts +1 -1
  336. package/src/providers/operations/provider-credential-service.ts +118 -51
  337. package/src/providers/operations/provider-rules/github.ts +48 -0
  338. package/src/providers/operations/provider-rules/grok.ts +33 -0
  339. package/src/providers/operations/provider-rules/inception.ts +49 -0
  340. package/src/providers/operations/provider-rules/index.ts +32 -0
  341. package/src/providers/operations/provider-rules/perch.ts +37 -0
  342. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  343. package/src/providers/operations/provider-rules/trae.ts +81 -0
  344. package/src/providers/operations/session-resolution.ts +17 -1
  345. package/src/providers/provider-contracts.ts +6 -4
  346. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  347. package/src/providers/provider-metadata.ts +70 -10
  348. package/src/providers/provider-probe.ts +15 -1
  349. package/src/providers/provider-registry.ts +42 -33
  350. package/src/providers/quota/quota-contracts.ts +2 -0
  351. package/src/providers/quota/quota-limit-windows.ts +16 -5
  352. package/src/providers/quota/quota-support.ts +1 -1
  353. package/src/providers/quota/quota-window-parser.ts +90 -18
  354. package/src/providers/quota/refresh.ts +100 -21
  355. package/src/security/api-key-auth-cache.ts +1 -1
  356. package/src/security/api-key-auth.ts +6 -2
  357. package/src/security/ip-boundary.ts +1 -1
  358. package/src/transport/dispatch/attempt-finalize.ts +15 -14
  359. package/src/transport/dispatch/attempt-loop.ts +73 -19
  360. package/src/transport/dispatch/generation.ts +2 -8
  361. package/src/transport/dispatch/handler-deps.ts +1 -1
  362. package/src/transport/dispatch/leases.ts +1 -1
  363. package/src/transport/dispatch/native-shared.ts +25 -0
  364. package/src/transport/dispatch/proxy-request.ts +8 -46
  365. package/src/transport/dispatch/responses-compact.ts +2 -8
  366. package/src/transport/dispatch/retry-policy.ts +1 -1
  367. package/src/transport/dispatch/streaming/finalize.ts +10 -7
  368. package/src/transport/dispatch/streaming/stream.ts +9 -37
  369. package/src/transport/dispatch/systemone.ts +2 -8
  370. package/src/transport/dispatch/token-count.ts +2 -6
  371. package/src/transport/dispatch/upstream.ts +63 -0
  372. package/src/transport/drain-endpoint.ts +31 -4
  373. package/src/transport/failure-policy.ts +16 -30
  374. package/src/transport/guards/body-policy.ts +7 -1
  375. package/src/transport/guards/error-lifecycle.ts +13 -1
  376. package/src/transport/guards/request-context.ts +22 -14
  377. package/src/transport/request/preparer.ts +81 -71
  378. package/src/transport/routing/bounded-lru.ts +14 -0
  379. package/src/transport/routing/drain-reserve.ts +129 -0
  380. package/src/transport/routing/plan-gate.ts +7 -1
  381. package/src/transport/routing/round-robin.ts +60 -0
  382. package/src/transport/routing/route-catalog.ts +123 -111
  383. package/src/transport/routing/route-model.ts +51 -9
  384. package/src/transport/routing/router.ts +393 -32
  385. package/src/transport/shutdown-notice.ts +1 -1
  386. package/src/transport/surface/adapters.ts +11 -2
  387. package/src/transport/surface/chat/adapter.ts +2 -2
  388. package/src/transport/surface/chat/encode.ts +47 -4
  389. package/src/transport/surface/chat/parse.ts +2 -12
  390. package/src/transport/surface/completion.ts +44 -33
  391. package/src/transport/surface/content-parts.ts +21 -0
  392. package/src/transport/surface/messages/adapter.ts +7 -1
  393. package/src/transport/surface/messages/encode.ts +11 -3
  394. package/src/transport/surface/messages/parse.ts +2 -1
  395. package/src/transport/surface/messages/stream.ts +33 -17
  396. package/src/transport/surface/responses/encode.ts +22 -5
  397. package/src/transport/surface/responses/parse.ts +3 -11
  398. package/src/workers/buddy-growth-tasks.ts +8 -8
  399. package/src/workers/daily-checkin.ts +4 -4
  400. package/src/workers/quota-refresh-sweep.ts +1 -1
  401. package/src/workers/zcode-gift-claim.ts +1 -1
  402. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
  403. package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
  404. package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
  405. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
  406. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
  407. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
  408. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
  409. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
  410. package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
  411. package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
  412. package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
  413. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
  414. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
  415. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
  416. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
  417. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
  418. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
  419. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js +0 -9
  420. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
  421. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
  422. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
  423. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
  424. package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
  425. package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
  426. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
  427. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
  428. package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
  429. package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
  430. package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
  431. package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
  432. package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
  433. package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
  434. package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
  435. package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
  436. package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
  437. package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
  438. package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
  439. package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
  440. package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
  441. package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
  442. package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
  443. package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
  444. package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
  445. package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
  446. package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
  447. package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
  448. package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
  449. package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
  450. package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
  451. package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
  452. package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
  453. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  454. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  455. package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
  456. package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
  457. package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
  458. package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
  459. package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
  460. package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
  461. package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
  462. package/src/network/pool-health.ts +0 -210
  463. package/src/providers/bundled-discovery-providers.ts +0 -98
  464. package/src/providers/configured-openai-providers.ts +0 -120
  465. package/src/providers/integrations/buddy/buddy-checkin.ts +0 -521
  466. package/src/providers/integrations/buddy/buddy-cli-headers.ts +0 -76
  467. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -130
  468. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +0 -234
  469. package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -496
  470. package/src/providers/integrations/buddy/buddy-identity.ts +0 -300
  471. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +0 -341
  472. package/src/providers/integrations/buddy/buddy-quota-shared.ts +0 -417
  473. package/src/providers/integrations/buddy/buddy-server-tools.ts +0 -240
  474. package/src/providers/integrations/buddy/buddy-usage-shared.ts +0 -221
  475. package/src/providers/integrations/buddy/buddy-usage-store.ts +0 -181
  476. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +0 -37
  477. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +0 -185
  478. package/src/providers/integrations/buddy/codebuddy/oauth.ts +0 -47
  479. package/src/providers/integrations/buddy/codebuddy/provider.ts +0 -175
  480. package/src/providers/integrations/buddy/codebuddy/quota.ts +0 -154
  481. package/src/providers/integrations/qoder/chat-proto.ts +0 -328
  482. package/src/providers/integrations/qoder/qoder-grpc.ts +0 -871
  483. package/src/providers/integrations/qoder/qoder-ide.ts +0 -588
  484. package/src/providers/operations/model-discovery-cache.ts +0 -34
  485. package/src/providers/operations/probe-fetch.ts +0 -15
  486. package/src/providers/operations/provider-version-cache.ts +0 -30
  487. package/src/providers/provider-capabilities.ts +0 -33
  488. package/src/providers/provider-credential-envelope.ts +0 -43
  489. /package/dist/dashboard/assets/{app-B7kIWFm5.js → app-C0gTXEoN.js} +0 -0
  490. /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-C0gTXEoN.js.gz} +0 -0
  491. /package/src/network/{http2-bidi.ts → bidi.ts} +0 -0
  492. /package/src/network/pool/{pool-scripts.ts → scripts.ts} +0 -0
  493. /package/src/network/pool/{pool-state.ts → state.ts} +0 -0
  494. /package/src/network/{backoff.ts → util/backoff.ts} +0 -0
  495. /package/src/network/{deduplication.ts → util/dedupe.ts} +0 -0
  496. /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, 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";
@@ -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 → { roster, fetchedAt } for the parameter lookup.
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 oldest-first eviction: the TTL is only checked on
92
- * read, so without a cap rotated tokens would grow the map without bound.
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 Map<string, { roster: readonly CursorModelInfo[]; fetchedAt: number }>();
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.delete(key); // re-set moves the entry to newest position
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[]; fetchedAt: number } | undefined =>
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 stream: ValidatedHttp2BidiStream;
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
- // Results previous legs already answered resolve nothing: the replayed
543
- // conversation carries them as history, not as an unanswered call. When
544
- // *no* call resolves AND the call id live nowhere in this process — a
545
- // gateway restart between legs, an evicted idle Run, then — the request is
546
- // by definition a complete replay (every answered call is present in the
547
- // conversation), and the fresh-turn path rebases it into a new Run.
548
- // A call id that is live under another scope keeps the strict refusal:
549
- // adopting that conversation would surface another tenant's Run under
550
- // this caller.
551
- if (
552
- resolvedAll.every((entry) => entry === undefined) &&
553
- isCompleteToolHistoryReplay(request, pending) &&
554
- pending.every((item) => !cursorRunSessions.hasAnyCall(item.callId))
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 (runId === undefined) {
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 (resolved.some((entry) => entry?.session.runId !== runId)) {
570
- throw new GatewayError(
571
- "invalid_request",
572
- 400,
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
- for (const item of pending) {
593
- const entry = cursorRunSessions.resolveCall(item.callId, scope);
594
- 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;
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
- 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;
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
- if (outboundHttp2 === undefined) {
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 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 🏗️",
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
- const opened = await openCursorRun({
667
- origin,
668
- credential,
669
- outboundHttp2,
670
- signal: context.abort_signal,
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
- stream: opened.stream,
675
- write: opened.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 opened.write(buildCancelAction(reason)).catch(() => undefined);
822
+ void transport.write(buildCancelAction(reason)).catch(() => undefined);
681
823
  }
682
- opened.stream.abort();
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 opened.write(
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
- 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);
738
940
  this.#handles.delete(runId);
739
- throw error;
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(() => undefined);
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
- const unsubscribeTerminal = handle.stream.onTerminal((reason) => {
799
- if (reason.kind === "local-close") return;
800
- const mapped = cursorRunTerminationError({ stream: handle.stream, reason });
801
- 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);
802
1060
  unsubscribeTerminal();
803
1061
  });
804
1062
  void (async () => {
805
1063
  try {
806
- for await (const message of decodeServerMessages(handle.stream.body)) {
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 && Date.now() - cached.fetchedAt < CURSOR_ROSTER_TTL_MS) {
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 cached?.roster ?? [];
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
- 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");
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
- const handled = await this.#handleExecServerMessage(execServer, session, handle);
932
- if (handled.kind !== "tool_call") continue;
933
- // Only MCP arms are relayed to the client for execution (client-
934
- // declared tools). Native tools are answered in-band by the executor.
935
- const callId = `call_cursor_${runId}_${handled.execId}`;
936
- const scope = context.continuation_scope;
937
- if (scope === undefined) {
938
- // Without a scope, no later request could be authorized to answer
939
- // this call, so the Run must not be held open for one.
940
- await handle.write(buildMcpRejected(handled.execId, "tool call is not answerable", handled.execIdString));
941
- continue;
942
- }
943
- if (!cursorRunSessions.registerPendingCall(runId, {
944
- callId,
945
- execId: handled.execId,
946
- ...(handled.execIdString === undefined ? {} : { execIdString: handled.execIdString }),
947
- toolName: handled.toolName,
948
- })) {
949
- await handle.write(buildMcpRejected(handled.execId, "tool call is not answerable", handled.execIdString));
950
- 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
+ };
951
1315
  }
952
- sawToolCall = true;
953
- yield {
954
- type: "tool_call_delta",
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
- // they report what Cursor's host did, not a call this gateway executes.
1018
- // 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.
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
- 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);
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 "you hit your usage limit"
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: failure.code, status: failure.status, message: failure.message },
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
-