@yansigit/opencodex 2.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (826) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/LICENSE +21 -0
  3. package/README.md +303 -0
  4. package/assets/architecture.png +0 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/claude-code-models.gif +0 -0
  7. package/assets/codex-app-picker.png +0 -0
  8. package/bin/ocx.mjs +587 -0
  9. package/bin/package-main.mjs +9 -0
  10. package/gui/dist/assets/index-BNJ7r4Gd.js +102 -0
  11. package/gui/dist/assets/index-CGoDO3uO.css +1 -0
  12. package/gui/dist/favicon.png +0 -0
  13. package/gui/dist/icons.svg +24 -0
  14. package/gui/dist/index.html +25 -0
  15. package/gui/dist/logo.png +0 -0
  16. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/cline-color.svg +16 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  25. package/gui/dist/provider-icons/discord.svg +1 -0
  26. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  27. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  28. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  29. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  30. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  31. package/gui/dist/provider-icons/grok.svg +1 -0
  32. package/gui/dist/provider-icons/groq-color.svg +1 -0
  33. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  34. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  35. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  36. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  37. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  38. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  39. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  40. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  41. package/gui/dist/provider-icons/openai.svg +1 -0
  42. package/gui/dist/provider-icons/opencode.svg +2 -0
  43. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  44. package/gui/dist/provider-icons/pi.svg +21 -0
  45. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  46. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  47. package/gui/dist/provider-icons/telegram.svg +1 -0
  48. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  49. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  50. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  51. package/package.json +108 -0
  52. package/src/AGENTS.md +28 -0
  53. package/src/adapters/anthropic-image-guard.ts +251 -0
  54. package/src/adapters/anthropic-image-normalize.ts +518 -0
  55. package/src/adapters/anthropic-output-schema.ts +137 -0
  56. package/src/adapters/anthropic.ts +1327 -0
  57. package/src/adapters/azure.ts +36 -0
  58. package/src/adapters/base.ts +121 -0
  59. package/src/adapters/client-fingerprint.ts +65 -0
  60. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  61. package/src/adapters/command-code.ts +601 -0
  62. package/src/adapters/cursor/arg-codec.ts +38 -0
  63. package/src/adapters/cursor/arg-normalize.ts +104 -0
  64. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  65. package/src/adapters/cursor/cursor-errors.ts +288 -0
  66. package/src/adapters/cursor/discovery.ts +333 -0
  67. package/src/adapters/cursor/effort-map.ts +151 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +250 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/h2-pool.ts +123 -0
  72. package/src/adapters/cursor/http1-bidi.ts +361 -0
  73. package/src/adapters/cursor/images.ts +704 -0
  74. package/src/adapters/cursor/kv-store.ts +52 -0
  75. package/src/adapters/cursor/live-models.ts +269 -0
  76. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  77. package/src/adapters/cursor/live-transport.ts +1653 -0
  78. package/src/adapters/cursor/mcp-config.ts +42 -0
  79. package/src/adapters/cursor/mcp-manager.ts +333 -0
  80. package/src/adapters/cursor/message-mapper.ts +49 -0
  81. package/src/adapters/cursor/native-exec-common.ts +76 -0
  82. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  83. package/src/adapters/cursor/native-exec-fs.ts +332 -0
  84. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  85. package/src/adapters/cursor/native-exec-network.ts +43 -0
  86. package/src/adapters/cursor/native-exec-shell.ts +547 -0
  87. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  88. package/src/adapters/cursor/native-exec.ts +663 -0
  89. package/src/adapters/cursor/protobuf-events.ts +1381 -0
  90. package/src/adapters/cursor/protobuf-request.ts +1032 -0
  91. package/src/adapters/cursor/request-builder.ts +461 -0
  92. package/src/adapters/cursor/thread-continuity.ts +67 -0
  93. package/src/adapters/cursor/tool-definitions.ts +735 -0
  94. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  95. package/src/adapters/cursor/transport-retry.ts +132 -0
  96. package/src/adapters/cursor/transport.ts +79 -0
  97. package/src/adapters/cursor/types.ts +79 -0
  98. package/src/adapters/cursor.ts +322 -0
  99. package/src/adapters/google-antigravity-hosts.ts +48 -0
  100. package/src/adapters/google-antigravity-replay.ts +827 -0
  101. package/src/adapters/google-antigravity-tools.ts +105 -0
  102. package/src/adapters/google-antigravity-wire.ts +141 -0
  103. package/src/adapters/google-errors.ts +92 -0
  104. package/src/adapters/google-http.ts +460 -0
  105. package/src/adapters/google-tool-schema.ts +238 -0
  106. package/src/adapters/google-truncation.ts +24 -0
  107. package/src/adapters/google-wire-compiler.ts +232 -0
  108. package/src/adapters/google.ts +1377 -0
  109. package/src/adapters/identity.ts +77 -0
  110. package/src/adapters/image.ts +23 -0
  111. package/src/adapters/kiro-constants.ts +16 -0
  112. package/src/adapters/kiro-errors.ts +208 -0
  113. package/src/adapters/kiro-events.ts +197 -0
  114. package/src/adapters/kiro-images.ts +129 -0
  115. package/src/adapters/kiro-retry.ts +312 -0
  116. package/src/adapters/kiro-thinking.ts +112 -0
  117. package/src/adapters/kiro-tool-fallback.ts +36 -0
  118. package/src/adapters/kiro-tools.ts +224 -0
  119. package/src/adapters/kiro-truncation.ts +33 -0
  120. package/src/adapters/kiro-wire.ts +129 -0
  121. package/src/adapters/kiro.ts +1936 -0
  122. package/src/adapters/mimo-free.ts +280 -0
  123. package/src/adapters/openai-chat-url.ts +11 -0
  124. package/src/adapters/openai-chat.ts +1980 -0
  125. package/src/adapters/openai-responses-url.ts +16 -0
  126. package/src/adapters/openai-responses.ts +1911 -0
  127. package/src/adapters/registry.ts +175 -0
  128. package/src/adapters/responses-tool-schema.ts +67 -0
  129. package/src/adapters/run-turn-queue.ts +114 -0
  130. package/src/adapters/tool-call-id.ts +119 -0
  131. package/src/adapters/tool-catalog-nudge.ts +154 -0
  132. package/src/adapters/upstream-http-error.ts +48 -0
  133. package/src/adapters/xai-web-search.ts +185 -0
  134. package/src/bridge.ts +1986 -0
  135. package/src/chat/inbound.ts +319 -0
  136. package/src/chat/outbound.ts +821 -0
  137. package/src/claude/agents-inject.ts +266 -0
  138. package/src/claude/alias.ts +149 -0
  139. package/src/claude/auth-detect.ts +229 -0
  140. package/src/claude/auth-mode-migration.ts +32 -0
  141. package/src/claude/auth-mode.ts +62 -0
  142. package/src/claude/context-windows.ts +205 -0
  143. package/src/claude/desktop-3p-guard.ts +35 -0
  144. package/src/claude/desktop-3p-paths.ts +84 -0
  145. package/src/claude/desktop-3p.ts +615 -0
  146. package/src/claude/desktop-health.ts +26 -0
  147. package/src/claude/desktop-profile.ts +263 -0
  148. package/src/claude/gateway-cache.ts +107 -0
  149. package/src/claude/inbound-debug.ts +163 -0
  150. package/src/claude/inbound.ts +578 -0
  151. package/src/claude/model-info.ts +174 -0
  152. package/src/claude/outbound.ts +926 -0
  153. package/src/cli/access.ts +108 -0
  154. package/src/cli/account-api.ts +302 -0
  155. package/src/cli/account-auth.ts +250 -0
  156. package/src/cli/account-catalog-refresh.ts +14 -0
  157. package/src/cli/account-extended.ts +737 -0
  158. package/src/cli/account-main.ts +317 -0
  159. package/src/cli/account.ts +299 -0
  160. package/src/cli/agent-driven.ts +70 -0
  161. package/src/cli/agent.ts +290 -0
  162. package/src/cli/catalog-prewarm.ts +27 -0
  163. package/src/cli/claude-agent-startup-sync.ts +73 -0
  164. package/src/cli/claude-desktop.ts +213 -0
  165. package/src/cli/claude.ts +355 -0
  166. package/src/cli/codex-log-guard-doctor.ts +103 -0
  167. package/src/cli/codex-shim-autorestore.ts +47 -0
  168. package/src/cli/codex-shim-readiness.ts +76 -0
  169. package/src/cli/combo.ts +127 -0
  170. package/src/cli/config-command.ts +209 -0
  171. package/src/cli/debug.ts +228 -0
  172. package/src/cli/dispatch.ts +585 -0
  173. package/src/cli/doctor.ts +1202 -0
  174. package/src/cli/ensure-desired-integrations.ts +152 -0
  175. package/src/cli/export-command.ts +213 -0
  176. package/src/cli/help.ts +101 -0
  177. package/src/cli/index.ts +973 -0
  178. package/src/cli/init.ts +211 -0
  179. package/src/cli/integrations.ts +260 -0
  180. package/src/cli/interactive-confirm.ts +133 -0
  181. package/src/cli/lab.ts +607 -0
  182. package/src/cli/launcher-context.ts +77 -0
  183. package/src/cli/minimax.ts +497 -0
  184. package/src/cli/models-runtime.ts +245 -0
  185. package/src/cli/models.ts +422 -0
  186. package/src/cli/observe.ts +206 -0
  187. package/src/cli/opencode.ts +588 -0
  188. package/src/cli/provider-replit.ts +232 -0
  189. package/src/cli/provider-runtime.ts +179 -0
  190. package/src/cli/provider.ts +492 -0
  191. package/src/cli/ready.ts +301 -0
  192. package/src/cli/registry.ts +422 -0
  193. package/src/cli/replit-gateway-key-input.ts +138 -0
  194. package/src/cli/root.ts +86 -0
  195. package/src/cli/route-policy.ts +92 -0
  196. package/src/cli/runtime-api.ts +328 -0
  197. package/src/cli/star-prompt.ts +211 -0
  198. package/src/cli/status-oauth.ts +78 -0
  199. package/src/cli/status.ts +328 -0
  200. package/src/cli/system-command.ts +112 -0
  201. package/src/cli/system-restart-client.ts +146 -0
  202. package/src/cli/tray-proxy.ts +199 -0
  203. package/src/cli/v2.ts +268 -0
  204. package/src/cli.ts +10 -0
  205. package/src/clients/config-export.ts +1704 -0
  206. package/src/codex/account-id.ts +34 -0
  207. package/src/codex/account-label.ts +47 -0
  208. package/src/codex/account-lifecycle.ts +172 -0
  209. package/src/codex/account-namespace-match.ts +63 -0
  210. package/src/codex/account-namespaces.ts +195 -0
  211. package/src/codex/account-pause.ts +20 -0
  212. package/src/codex/account-priority.ts +83 -0
  213. package/src/codex/account-runtime-state.ts +31 -0
  214. package/src/codex/account-store.ts +544 -0
  215. package/src/codex/account-usability.ts +43 -0
  216. package/src/codex/admission.ts +256 -0
  217. package/src/codex/affinity-debug.ts +162 -0
  218. package/src/codex/agent-roles-sync.ts +225 -0
  219. package/src/codex/agent-roles.ts +238 -0
  220. package/src/codex/app-server-processes.ts +1143 -0
  221. package/src/codex/app-server-restart-service.ts +232 -0
  222. package/src/codex/auth-api.ts +2147 -0
  223. package/src/codex/auth-collision.ts +109 -0
  224. package/src/codex/auth-context.ts +665 -0
  225. package/src/codex/autostart-health.ts +156 -0
  226. package/src/codex/catalog/account-models.ts +67 -0
  227. package/src/codex/catalog/aggregation.ts +436 -0
  228. package/src/codex/catalog/bundled.ts +549 -0
  229. package/src/codex/catalog/effort.ts +446 -0
  230. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  231. package/src/codex/catalog/kinds.ts +2 -0
  232. package/src/codex/catalog/metadata.ts +664 -0
  233. package/src/codex/catalog/native-models.ts +72 -0
  234. package/src/codex/catalog/parsing.ts +650 -0
  235. package/src/codex/catalog/provider-fetch.ts +2064 -0
  236. package/src/codex/catalog/sync.ts +1883 -0
  237. package/src/codex/catalog-admission.ts +199 -0
  238. package/src/codex/catalog-refresh-status.ts +105 -0
  239. package/src/codex/catalog-write-serialization.ts +242 -0
  240. package/src/codex/catalog.ts +14 -0
  241. package/src/codex/codex-write-lock.ts +384 -0
  242. package/src/codex/convergence-types.ts +614 -0
  243. package/src/codex/convergence.ts +651 -0
  244. package/src/codex/coordinator-doctor.ts +332 -0
  245. package/src/codex/custom-model-catalog-migration.ts +176 -0
  246. package/src/codex/data/upstream-models.json +830 -0
  247. package/src/codex/desired-state.ts +230 -0
  248. package/src/codex/exec-invocation.ts +22 -0
  249. package/src/codex/features.ts +1566 -0
  250. package/src/codex/generation.ts +202 -0
  251. package/src/codex/history-job.ts +407 -0
  252. package/src/codex/history-lock.ts +242 -0
  253. package/src/codex/history-migration-guardian.ts +108 -0
  254. package/src/codex/history-provider.ts +979 -0
  255. package/src/codex/history-transition.ts +105 -0
  256. package/src/codex/history-worker.ts +220 -0
  257. package/src/codex/home.ts +206 -0
  258. package/src/codex/inject-coordination.ts +290 -0
  259. package/src/codex/inject.ts +1733 -0
  260. package/src/codex/injected-marker.ts +106 -0
  261. package/src/codex/integration-record.ts +266 -0
  262. package/src/codex/internal/catalog-writer.ts +203 -0
  263. package/src/codex/internal/history-writer.ts +80 -0
  264. package/src/codex/journal.ts +225 -0
  265. package/src/codex/log-guard/inspect.ts +506 -0
  266. package/src/codex/log-guard/lock.ts +150 -0
  267. package/src/codex/log-guard/maintenance.ts +403 -0
  268. package/src/codex/log-guard/path-safety.ts +88 -0
  269. package/src/codex/log-guard/policy.ts +44 -0
  270. package/src/codex/log-guard/processes.ts +205 -0
  271. package/src/codex/log-guard/protection.ts +489 -0
  272. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  273. package/src/codex/main-account-cache.ts +56 -0
  274. package/src/codex/main-account.ts +68 -0
  275. package/src/codex/management-convergence.ts +167 -0
  276. package/src/codex/model-cache.ts +273 -0
  277. package/src/codex/model-entitlements.ts +353 -0
  278. package/src/codex/native-main-admission.ts +47 -0
  279. package/src/codex/native-main-auth-temp.ts +187 -0
  280. package/src/codex/native-main-claim.ts +178 -0
  281. package/src/codex/native-main-lock-file.ts +162 -0
  282. package/src/codex/native-main-owner.ts +329 -0
  283. package/src/codex/native-profile-api.ts +247 -0
  284. package/src/codex/native-profile-manager.ts +1531 -0
  285. package/src/codex/native-profile-processes.ts +121 -0
  286. package/src/codex/native-profile-recovery.ts +99 -0
  287. package/src/codex/native-profile-stage-store.ts +387 -0
  288. package/src/codex/native-profile-startup.ts +492 -0
  289. package/src/codex/native-profile-store.ts +855 -0
  290. package/src/codex/native-profile-types.ts +120 -0
  291. package/src/codex/native-residue.ts +682 -0
  292. package/src/codex/paths.ts +144 -0
  293. package/src/codex/plan-from-token.ts +140 -0
  294. package/src/codex/plan.ts +40 -0
  295. package/src/codex/plugins-doctor.ts +242 -0
  296. package/src/codex/pool-rotation.ts +295 -0
  297. package/src/codex/project-config-warnings.ts +425 -0
  298. package/src/codex/prompt-journal.ts +352 -0
  299. package/src/codex/prompt-layers.ts +967 -0
  300. package/src/codex/prompt-lock.ts +143 -0
  301. package/src/codex/quota-rejection.ts +298 -0
  302. package/src/codex/quota.ts +573 -0
  303. package/src/codex/refresh.ts +62 -0
  304. package/src/codex/reset-credit-recovery.ts +1044 -0
  305. package/src/codex/routing.ts +1888 -0
  306. package/src/codex/runtime.ts +659 -0
  307. package/src/codex/shim.ts +2170 -0
  308. package/src/codex/subagent-defaults.ts +550 -0
  309. package/src/codex/subagent-model-fallback.ts +784 -0
  310. package/src/codex/sync.ts +319 -0
  311. package/src/codex/transition-state.ts +612 -0
  312. package/src/codex/upstream-host-health.ts +368 -0
  313. package/src/codex/user-identity.ts +557 -0
  314. package/src/codex/warmup.ts +298 -0
  315. package/src/codex/websocket-registry.ts +100 -0
  316. package/src/codex/write-coordination.ts +114 -0
  317. package/src/combos/failover.ts +160 -0
  318. package/src/combos/index.ts +45 -0
  319. package/src/combos/request.ts +94 -0
  320. package/src/combos/resolve.ts +232 -0
  321. package/src/combos/types.ts +398 -0
  322. package/src/config/provider-name.ts +24 -0
  323. package/src/config.ts +4041 -0
  324. package/src/fork/register.ts +3 -0
  325. package/src/generated/compatibility-version.json +3116 -0
  326. package/src/generated/model-metadata.ts +106 -0
  327. package/src/github/star-state.ts +203 -0
  328. package/src/grok/inject.ts +530 -0
  329. package/src/grok/inspect.ts +45 -0
  330. package/src/grok/status.ts +121 -0
  331. package/src/grok/sync.ts +66 -0
  332. package/src/images/artifacts.ts +516 -0
  333. package/src/images/fulfill-video.ts +163 -0
  334. package/src/images/fulfill.ts +149 -0
  335. package/src/images/index.ts +4 -0
  336. package/src/images/loop.ts +955 -0
  337. package/src/images/plan.ts +143 -0
  338. package/src/images/synthetic-tool.ts +133 -0
  339. package/src/images/types.ts +41 -0
  340. package/src/images/xai-client.ts +141 -0
  341. package/src/images/xai-video-client.ts +163 -0
  342. package/src/index.ts +22 -0
  343. package/src/integrations/config-io.ts +269 -0
  344. package/src/integrations/journal.ts +315 -0
  345. package/src/integrations/merge.ts +135 -0
  346. package/src/integrations/mutation-flight.ts +71 -0
  347. package/src/integrations/native/ownership-preflight.ts +202 -0
  348. package/src/integrations/omp-yaml-source.ts +358 -0
  349. package/src/integrations/owned-refresh.ts +74 -0
  350. package/src/integrations/ownership.ts +111 -0
  351. package/src/integrations/registry.ts +159 -0
  352. package/src/integrations/serialize.ts +314 -0
  353. package/src/integrations/state.ts +361 -0
  354. package/src/integrations/store.ts +103 -0
  355. package/src/integrations/writer-lock.ts +98 -0
  356. package/src/integrations/writer.ts +691 -0
  357. package/src/lab/artifacts/sanitize.ts +586 -0
  358. package/src/lab/artifacts/secure-fs.ts +475 -0
  359. package/src/lab/artifacts/store.ts +310 -0
  360. package/src/lab/automation/budgets.ts +78 -0
  361. package/src/lab/automation/config-persistence.ts +256 -0
  362. package/src/lab/automation/constants.ts +39 -0
  363. package/src/lab/automation/cooldown.ts +103 -0
  364. package/src/lab/automation/dispatch.ts +211 -0
  365. package/src/lab/automation/index.ts +13 -0
  366. package/src/lab/automation/orchestrator.ts +499 -0
  367. package/src/lab/automation/persistence.ts +512 -0
  368. package/src/lab/automation/planner.ts +371 -0
  369. package/src/lab/automation/policy.ts +136 -0
  370. package/src/lab/automation/queue.ts +191 -0
  371. package/src/lab/automation/recovery.ts +24 -0
  372. package/src/lab/automation/route-context.ts +21 -0
  373. package/src/lab/automation/run-key.ts +44 -0
  374. package/src/lab/automation/runs-query.ts +34 -0
  375. package/src/lab/automation/types.ts +160 -0
  376. package/src/lab/conformance/assertion.ts +325 -0
  377. package/src/lab/conformance/digest.ts +22 -0
  378. package/src/lab/conformance/executor.ts +741 -0
  379. package/src/lab/conformance/fixture-provider.ts +27 -0
  380. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  381. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  382. package/src/lab/conformance/harness-budget.ts +47 -0
  383. package/src/lab/conformance/index.ts +5 -0
  384. package/src/lab/conformance/jcs.ts +64 -0
  385. package/src/lab/conformance/json-pointer.ts +39 -0
  386. package/src/lab/conformance/manifest.ts +180 -0
  387. package/src/lab/conformance/mcp-stub.ts +179 -0
  388. package/src/lab/conformance/negative-controls.ts +164 -0
  389. package/src/lab/conformance/observation.ts +355 -0
  390. package/src/lab/conformance/runner.ts +68 -0
  391. package/src/lab/conformance/sse-normalize.ts +59 -0
  392. package/src/lab/conformance/suite-manifest.ts +78 -0
  393. package/src/lab/conformance/types.ts +214 -0
  394. package/src/lab/constants.ts +126 -0
  395. package/src/lab/digest.ts +64 -0
  396. package/src/lab/events/errors.ts +9 -0
  397. package/src/lab/events/limits.ts +117 -0
  398. package/src/lab/events/types.ts +229 -0
  399. package/src/lab/events/validate.ts +781 -0
  400. package/src/lab/fabric/constants.ts +40 -0
  401. package/src/lab/fabric/executor.ts +492 -0
  402. package/src/lab/fabric/index.ts +80 -0
  403. package/src/lab/fabric/manifest.ts +222 -0
  404. package/src/lab/fabric/observe.ts +489 -0
  405. package/src/lab/fabric/patch.ts +79 -0
  406. package/src/lab/fabric/producer-child.ts +139 -0
  407. package/src/lab/fabric/producer-isolate.ts +276 -0
  408. package/src/lab/fabric/producer-protocol.ts +61 -0
  409. package/src/lab/fabric/scratch.ts +439 -0
  410. package/src/lab/fabric/subject.ts +106 -0
  411. package/src/lab/fabric/types.ts +134 -0
  412. package/src/lab/fabric/verifier.ts +98 -0
  413. package/src/lab/index.ts +54 -0
  414. package/src/lab/ledger/artifact-refs.ts +127 -0
  415. package/src/lab/ledger/invalidation.ts +136 -0
  416. package/src/lab/ledger/purge.ts +310 -0
  417. package/src/lab/ledger/store.ts +532 -0
  418. package/src/lab/live/credential-lease.ts +53 -0
  419. package/src/lab/live/destination.ts +155 -0
  420. package/src/lab/live/executor.ts +336 -0
  421. package/src/lab/live/inert-tools.ts +56 -0
  422. package/src/lab/live/manifest.ts +85 -0
  423. package/src/lab/live/mcp-loopback.ts +57 -0
  424. package/src/lab/live/runner.ts +19 -0
  425. package/src/lab/live/sandbox.ts +61 -0
  426. package/src/lab/live/suite-manifest.ts +41 -0
  427. package/src/lab/live/transport.ts +118 -0
  428. package/src/lab/live/types.ts +197 -0
  429. package/src/lab/observe/from-conformance.ts +301 -0
  430. package/src/lab/observe/from-live.ts +117 -0
  431. package/src/lab/paths.ts +153 -0
  432. package/src/lab/projection/rebuild.ts +495 -0
  433. package/src/lab/projection/schema.ts +135 -0
  434. package/src/lab/projection/verdicts.ts +474 -0
  435. package/src/lab/projection/verification.ts +412 -0
  436. package/src/lab/public/bundle.ts +217 -0
  437. package/src/lab/public/community-authority.ts +175 -0
  438. package/src/lab/public/community-files.ts +29 -0
  439. package/src/lab/public/community.ts +479 -0
  440. package/src/lab/public/file-safety.ts +155 -0
  441. package/src/lab/public/ids.ts +26 -0
  442. package/src/lab/public/index.ts +16 -0
  443. package/src/lab/public/mutation-lock.ts +424 -0
  444. package/src/lab/public/operator.ts +353 -0
  445. package/src/lab/public/origin-purge.ts +79 -0
  446. package/src/lab/public/origin.ts +203 -0
  447. package/src/lab/public/privacy.ts +143 -0
  448. package/src/lab/public/private-file.ts +261 -0
  449. package/src/lab/public/project.ts +124 -0
  450. package/src/lab/public/purge-test-fault.ts +21 -0
  451. package/src/lab/public/purge.ts +223 -0
  452. package/src/lab/public/registry.ts +44 -0
  453. package/src/lab/public/revocation.ts +252 -0
  454. package/src/lab/public/signature.ts +243 -0
  455. package/src/lab/public/storage.ts +105 -0
  456. package/src/lab/public/strict-json.ts +206 -0
  457. package/src/lab/public/time.ts +26 -0
  458. package/src/lab/public/types.ts +172 -0
  459. package/src/lab/public/validate.ts +391 -0
  460. package/src/lab/query/catalog.ts +101 -0
  461. package/src/lab/query/connection.ts +107 -0
  462. package/src/lab/query/constants.ts +4 -0
  463. package/src/lab/query/cursor.ts +132 -0
  464. package/src/lab/query/dto-map.ts +277 -0
  465. package/src/lab/query/errors.ts +22 -0
  466. package/src/lab/query/freshness.ts +53 -0
  467. package/src/lab/query/index.ts +45 -0
  468. package/src/lab/query/latest-observation.ts +59 -0
  469. package/src/lab/query/passive-production.ts +159 -0
  470. package/src/lab/query/queries.ts +444 -0
  471. package/src/lab/query/types.ts +266 -0
  472. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  473. package/src/lab/subject/installation-salt.ts +112 -0
  474. package/src/lab/subject/protocol-subject.ts +80 -0
  475. package/src/lab/subject/route-subject.ts +74 -0
  476. package/src/lib/abort.ts +146 -0
  477. package/src/lib/admin-secrets.ts +25 -0
  478. package/src/lib/admission.ts +83 -0
  479. package/src/lib/app-owned-memory-stores.ts +195 -0
  480. package/src/lib/app-owned-memory.ts +265 -0
  481. package/src/lib/bounded-body.ts +346 -0
  482. package/src/lib/bun-binary-validator.d.mts +3 -0
  483. package/src/lib/bun-binary-validator.mjs +18 -0
  484. package/src/lib/bun-runtime.ts +184 -0
  485. package/src/lib/bun-stream-caps.ts +130 -0
  486. package/src/lib/codex-restart-contract.ts +120 -0
  487. package/src/lib/config-ownership.ts +364 -0
  488. package/src/lib/crash-guard.ts +344 -0
  489. package/src/lib/debug-log-buffer.ts +83 -0
  490. package/src/lib/debug-settings.ts +108 -0
  491. package/src/lib/debug.ts +31 -0
  492. package/src/lib/destination-policy.ts +380 -0
  493. package/src/lib/errors.ts +406 -0
  494. package/src/lib/eventstream-decoder.ts +253 -0
  495. package/src/lib/fabric-task-execution-authority.ts +7 -0
  496. package/src/lib/fabric-task-host.ts +29 -0
  497. package/src/lib/gcp-adc.ts +341 -0
  498. package/src/lib/injection-debug-log.ts +58 -0
  499. package/src/lib/lab-activation.ts +223 -0
  500. package/src/lib/lab-live-execution-authority.ts +13 -0
  501. package/src/lib/lab-live-host.ts +30 -0
  502. package/src/lib/lab-live-pinned-sender.ts +56 -0
  503. package/src/lib/lab-live-route-production.ts +130 -0
  504. package/src/lib/lab-passive-linker-registration.ts +26 -0
  505. package/src/lib/local-management-attestation.ts +51 -0
  506. package/src/lib/local-management-capability.ts +100 -0
  507. package/src/lib/local-provider-reload-contract.ts +100 -0
  508. package/src/lib/open-url.ts +25 -0
  509. package/src/lib/optional-shutdown-hooks.ts +57 -0
  510. package/src/lib/pinned-http.ts +270 -0
  511. package/src/lib/privacy.ts +20 -0
  512. package/src/lib/process-control.ts +168 -0
  513. package/src/lib/provider-outbound.ts +210 -0
  514. package/src/lib/provider-url.ts +14 -0
  515. package/src/lib/proxy-env.ts +18 -0
  516. package/src/lib/redact.ts +521 -0
  517. package/src/lib/retry-after.ts +55 -0
  518. package/src/lib/self-launch-argv.ts +15 -0
  519. package/src/lib/server-resource-ownership.ts +71 -0
  520. package/src/lib/service-secrets.ts +25 -0
  521. package/src/lib/shadow-call.ts +61 -0
  522. package/src/lib/sidecar-tracker.ts +52 -0
  523. package/src/lib/sse-decoder.ts +364 -0
  524. package/src/lib/state-store-registrations.ts +119 -0
  525. package/src/lib/state-store-sweeper.ts +184 -0
  526. package/src/lib/system-restart-contract.ts +73 -0
  527. package/src/lib/test-home-guard.ts +90 -0
  528. package/src/lib/token-estimate.ts +86 -0
  529. package/src/lib/tool-argument-integers.ts +202 -0
  530. package/src/lib/translator-budget.ts +400 -0
  531. package/src/lib/upstream-http-version.ts +57 -0
  532. package/src/lib/upstream-reachability.ts +95 -0
  533. package/src/lib/upstream-retry.ts +392 -0
  534. package/src/lib/win-exec.ts +115 -0
  535. package/src/lib/win-paths.ts +68 -0
  536. package/src/lib/windows-atomic-replace.ts +156 -0
  537. package/src/lib/windows-elevation.ts +773 -0
  538. package/src/lib/windows-secret-acl.ts +854 -0
  539. package/src/lib/windows-service-wrappers.ts +72 -0
  540. package/src/lib/windows-text.ts +106 -0
  541. package/src/lib/windows-user-principal.ts +341 -0
  542. package/src/lib/winsw.ts +403 -0
  543. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  544. package/src/oauth/account-import/index.ts +15 -0
  545. package/src/oauth/account-import/parser.ts +83 -0
  546. package/src/oauth/account-import/registry.ts +18 -0
  547. package/src/oauth/account-import/service.ts +75 -0
  548. package/src/oauth/account-import/types.ts +91 -0
  549. package/src/oauth/anthropic-routing.ts +594 -0
  550. package/src/oauth/anthropic.ts +188 -0
  551. package/src/oauth/antigravity-routing.ts +151 -0
  552. package/src/oauth/callback-server.ts +300 -0
  553. package/src/oauth/chatgpt.ts +161 -0
  554. package/src/oauth/command-code.ts +239 -0
  555. package/src/oauth/cursor.ts +252 -0
  556. package/src/oauth/github-copilot.ts +428 -0
  557. package/src/oauth/google-antigravity.ts +262 -0
  558. package/src/oauth/health.ts +407 -0
  559. package/src/oauth/index.ts +1504 -0
  560. package/src/oauth/key-providers.ts +124 -0
  561. package/src/oauth/kimi.ts +227 -0
  562. package/src/oauth/kiro-credentials.ts +726 -0
  563. package/src/oauth/kiro.ts +621 -0
  564. package/src/oauth/local-token-detect.ts +130 -0
  565. package/src/oauth/log.ts +50 -0
  566. package/src/oauth/login-cli.ts +223 -0
  567. package/src/oauth/nous.ts +798 -0
  568. package/src/oauth/pkce.ts +15 -0
  569. package/src/oauth/store.ts +728 -0
  570. package/src/oauth/token-guardian.ts +309 -0
  571. package/src/oauth/types.ts +62 -0
  572. package/src/oauth/xai.ts +241 -0
  573. package/src/providers/alibaba-region-backup.ts +75 -0
  574. package/src/providers/alibaba-region-migration.ts +156 -0
  575. package/src/providers/alibaba-region-startup.ts +36 -0
  576. package/src/providers/antigravity-models.ts +695 -0
  577. package/src/providers/antigravity-quota.ts +216 -0
  578. package/src/providers/api-keys.ts +140 -0
  579. package/src/providers/base-url-choices.ts +74 -0
  580. package/src/providers/codex-capacity.ts +292 -0
  581. package/src/providers/command-code-efforts.ts +144 -0
  582. package/src/providers/context-cap.ts +82 -0
  583. package/src/providers/cursor-pool.ts +72 -0
  584. package/src/providers/derive.ts +586 -0
  585. package/src/providers/fastwire.ts +501 -0
  586. package/src/providers/free-directory.ts +187 -0
  587. package/src/providers/github-copilot-transport.ts +56 -0
  588. package/src/providers/google-vertex-location.ts +14 -0
  589. package/src/providers/key-failover.ts +271 -0
  590. package/src/providers/kiro-models.ts +67 -0
  591. package/src/providers/label.ts +19 -0
  592. package/src/providers/model-discovery-limits.ts +16 -0
  593. package/src/providers/model-discovery.ts +449 -0
  594. package/src/providers/model-rename-migration.ts +255 -0
  595. package/src/providers/model-rename-startup.ts +28 -0
  596. package/src/providers/openai-sidecar.ts +243 -0
  597. package/src/providers/openai-tier-startup.ts +56 -0
  598. package/src/providers/openai-tiers.ts +423 -0
  599. package/src/providers/openai-virtual-models.ts +83 -0
  600. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  601. package/src/providers/openrouter-routing.ts +102 -0
  602. package/src/providers/provider-id-rewrite.ts +185 -0
  603. package/src/providers/quota.ts +2345 -0
  604. package/src/providers/registry.ts +2918 -0
  605. package/src/providers/replit/constants.ts +27 -0
  606. package/src/providers/replit/derive.ts +85 -0
  607. package/src/providers/replit/headers.ts +28 -0
  608. package/src/providers/replit/origin.ts +55 -0
  609. package/src/providers/replit/pair-install-response.ts +72 -0
  610. package/src/providers/replit/probe.ts +199 -0
  611. package/src/providers/replit/setup.ts +350 -0
  612. package/src/providers/request-pacing.ts +310 -0
  613. package/src/providers/service-tier.ts +277 -0
  614. package/src/providers/slug-codec.ts +103 -0
  615. package/src/providers/static-model-discovery.ts +86 -0
  616. package/src/providers/xai-responses-opt-in.ts +15 -0
  617. package/src/providers/xai-transport.ts +148 -0
  618. package/src/reasoning-effort.ts +183 -0
  619. package/src/responses/compaction.ts +142 -0
  620. package/src/responses/custom-tool-compat.ts +266 -0
  621. package/src/responses/hosted-tool-policy.ts +9 -0
  622. package/src/responses/namespace-tool-compat.ts +355 -0
  623. package/src/responses/parser.ts +838 -0
  624. package/src/responses/provider-continuation.ts +98 -0
  625. package/src/responses/provider-opaque-metadata.ts +73 -0
  626. package/src/responses/reasoning-envelope.ts +60 -0
  627. package/src/responses/reasoning-replay-cache.ts +426 -0
  628. package/src/responses/schema.ts +165 -0
  629. package/src/responses/spill-store.ts +459 -0
  630. package/src/responses/state.ts +1433 -0
  631. package/src/responses/thought-signature-replay.ts +347 -0
  632. package/src/responses/tool-groups.ts +19 -0
  633. package/src/responses/tool-search-compat.ts +301 -0
  634. package/src/responses/truncated-stop-reason.ts +60 -0
  635. package/src/router.ts +761 -0
  636. package/src/routing/analytics.ts +378 -0
  637. package/src/routing/capability.ts +244 -0
  638. package/src/routing/compatibility/assemble.ts +73 -0
  639. package/src/routing/compatibility/behavior.ts +278 -0
  640. package/src/routing/compatibility/catalog.ts +99 -0
  641. package/src/routing/compatibility/endpoint.ts +52 -0
  642. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  643. package/src/routing/compatibility/policy.ts +181 -0
  644. package/src/routing/compatibility/provider-slot.ts +56 -0
  645. package/src/routing/compatibility/reader.ts +110 -0
  646. package/src/routing/compatibility/subject.ts +191 -0
  647. package/src/routing/compatibility/types.ts +64 -0
  648. package/src/routing/compatibility/version.ts +104 -0
  649. package/src/routing/cost.ts +77 -0
  650. package/src/routing/evaluator.ts +495 -0
  651. package/src/routing/health.ts +412 -0
  652. package/src/routing/history/cursor.ts +43 -0
  653. package/src/routing/history/indexer.ts +605 -0
  654. package/src/routing/history/schema.ts +72 -0
  655. package/src/routing/profile-namespace.ts +15 -0
  656. package/src/routing/profile.ts +547 -0
  657. package/src/routing/quota.ts +145 -0
  658. package/src/routing/request-evidence.ts +45 -0
  659. package/src/routing/trace.ts +776 -0
  660. package/src/server/adapter-resolve.ts +53 -0
  661. package/src/server/auth-cors.ts +751 -0
  662. package/src/server/background-lifecycle.ts +182 -0
  663. package/src/server/chat-completions.ts +442 -0
  664. package/src/server/chat-native-sse.ts +331 -0
  665. package/src/server/chat-native.ts +426 -0
  666. package/src/server/claude-messages.ts +1030 -0
  667. package/src/server/direct-local-http.ts +347 -0
  668. package/src/server/effort-policy.ts +190 -0
  669. package/src/server/github-copilot-responses-repair.ts +338 -0
  670. package/src/server/gui-static.ts +152 -0
  671. package/src/server/image-retry.ts +42 -0
  672. package/src/server/images.ts +568 -0
  673. package/src/server/index.ts +1813 -0
  674. package/src/server/lifecycle.ts +498 -0
  675. package/src/server/live.ts +717 -0
  676. package/src/server/local-management-read-client.ts +90 -0
  677. package/src/server/local-provider-reload-client.ts +137 -0
  678. package/src/server/management/agent-settings-routes.ts +1433 -0
  679. package/src/server/management/api-access.ts +141 -0
  680. package/src/server/management/api-key-usage.ts +193 -0
  681. package/src/server/management/body.ts +41 -0
  682. package/src/server/management/combo-routes.ts +263 -0
  683. package/src/server/management/config-routes.ts +835 -0
  684. package/src/server/management/context.ts +113 -0
  685. package/src/server/management/integration-routes.ts +498 -0
  686. package/src/server/management/lab-automation-routes.ts +206 -0
  687. package/src/server/management/lab-routes.ts +563 -0
  688. package/src/server/management/logs-usage-routes.ts +586 -0
  689. package/src/server/management/model-routes.ts +560 -0
  690. package/src/server/management/model-rows.ts +163 -0
  691. package/src/server/management/native-integration-routes.ts +769 -0
  692. package/src/server/management/oauth-account-routes.ts +637 -0
  693. package/src/server/management/provider-capability-config.ts +48 -0
  694. package/src/server/management/provider-routes.ts +1033 -0
  695. package/src/server/management/replit-provider-routes.ts +86 -0
  696. package/src/server/management/request-history-routes.ts +191 -0
  697. package/src/server/management/routing-analytics-routes.ts +74 -0
  698. package/src/server/management/routing-profile-routes.ts +385 -0
  699. package/src/server/management/shared.ts +286 -0
  700. package/src/server/management/sidebar-routes.ts +106 -0
  701. package/src/server/management/storage-log-guard-routes.ts +186 -0
  702. package/src/server/management/sync-response.ts +69 -0
  703. package/src/server/management/system-restart.ts +435 -0
  704. package/src/server/management/system-routes.ts +194 -0
  705. package/src/server/management/usage-summary-cache.ts +94 -0
  706. package/src/server/management/vision-sidecar-options.ts +167 -0
  707. package/src/server/management/web-search-sidecar-options.ts +120 -0
  708. package/src/server/management-api.ts +314 -0
  709. package/src/server/management-auth.ts +482 -0
  710. package/src/server/memory-watchdog.ts +156 -0
  711. package/src/server/passive-route-linker.ts +66 -0
  712. package/src/server/port-reclaim.ts +307 -0
  713. package/src/server/ports.ts +156 -0
  714. package/src/server/proxy-liveness.ts +328 -0
  715. package/src/server/readiness.ts +99 -0
  716. package/src/server/relay-eager.ts +353 -0
  717. package/src/server/relay.ts +1209 -0
  718. package/src/server/request-decompress.ts +239 -0
  719. package/src/server/request-log-conversation.ts +168 -0
  720. package/src/server/request-log.ts +1259 -0
  721. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  722. package/src/server/responses/agent-task-recovery.ts +465 -0
  723. package/src/server/responses/collaboration.ts +551 -0
  724. package/src/server/responses/compact.ts +771 -0
  725. package/src/server/responses/core.ts +5389 -0
  726. package/src/server/responses/empty-completion-guard.ts +276 -0
  727. package/src/server/responses/encrypted-payload.ts +331 -0
  728. package/src/server/responses/fetch-helpers.ts +232 -0
  729. package/src/server/responses/input-admission.ts +185 -0
  730. package/src/server/responses/pacing-overload.ts +13 -0
  731. package/src/server/responses/passthrough-error.ts +78 -0
  732. package/src/server/responses/policy-fallback.ts +178 -0
  733. package/src/server/responses/responses-field-backfill.ts +251 -0
  734. package/src/server/responses/terminal-guard.ts +251 -0
  735. package/src/server/responses/upstream-error.ts +53 -0
  736. package/src/server/responses/ws-upstream.ts +308 -0
  737. package/src/server/responses-custom-tool-repair.ts +282 -0
  738. package/src/server/responses-image-gen-repair.ts +132 -0
  739. package/src/server/responses-item-id-repair.ts +272 -0
  740. package/src/server/responses-json-events.ts +90 -0
  741. package/src/server/responses-model-rewrite.ts +29 -0
  742. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  743. package/src/server/responses-snapshot-repair.ts +621 -0
  744. package/src/server/responses-terminal-repair.ts +342 -0
  745. package/src/server/responses-tool-search-repair.ts +267 -0
  746. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  747. package/src/server/responses.ts +25 -0
  748. package/src/server/search.ts +201 -0
  749. package/src/server/sse-frame-buffer.ts +292 -0
  750. package/src/server/sse-payload-rewrite.ts +263 -0
  751. package/src/server/startup-action-control.ts +315 -0
  752. package/src/server/startup-health-cache.ts +131 -0
  753. package/src/server/system-env.ts +484 -0
  754. package/src/server/windows-tcp-drop.ts +184 -0
  755. package/src/server/windows-tray-control.ts +41 -0
  756. package/src/server/ws-bridge.ts +472 -0
  757. package/src/service-manager-probe.ts +892 -0
  758. package/src/service.ts +3575 -0
  759. package/src/sidecar/auth.ts +92 -0
  760. package/src/sidecar/candidates.ts +83 -0
  761. package/src/stall-timeout.ts +20 -0
  762. package/src/storage/cleanup-job.ts +57 -0
  763. package/src/storage/cleanup.ts +3085 -0
  764. package/src/storage/policy-job.ts +457 -0
  765. package/src/storage/policy-scheduler.ts +40 -0
  766. package/src/storage/policy-worker.ts +59 -0
  767. package/src/storage/policy.ts +527 -0
  768. package/src/storage/restore-job.ts +299 -0
  769. package/src/storage/restore-worker.ts +58 -0
  770. package/src/storage/scanner.ts +238 -0
  771. package/src/storage/storage-mutation-coordinator.ts +139 -0
  772. package/src/storage/worker-lifecycle.ts +215 -0
  773. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  774. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  775. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  776. package/src/tray/assets/opencodex-tray.png +0 -0
  777. package/src/tray/windows-tray.ps1 +364 -0
  778. package/src/tray/windows.ts +757 -0
  779. package/src/types/accounts.ts +37 -0
  780. package/src/types/config.ts +876 -0
  781. package/src/types/provider.ts +545 -0
  782. package/src/types/request.ts +384 -0
  783. package/src/types/tools.ts +131 -0
  784. package/src/types/wire.ts +80 -0
  785. package/src/types.ts +106 -0
  786. package/src/update/badge.ts +72 -0
  787. package/src/update/index.ts +415 -0
  788. package/src/update/job.ts +1887 -0
  789. package/src/update/notify.ts +263 -0
  790. package/src/update/npm-cache-preflight.d.mts +47 -0
  791. package/src/update/npm-cache-preflight.mjs +201 -0
  792. package/src/update/npm-invocation.d.mts +23 -0
  793. package/src/update/npm-invocation.mjs +94 -0
  794. package/src/update/transactional-install.d.mts +22 -0
  795. package/src/update/transactional-install.mjs +259 -0
  796. package/src/update/tray-update-plan.d.mts +18 -0
  797. package/src/update/tray-update-plan.mjs +38 -0
  798. package/src/usage/cost.ts +625 -0
  799. package/src/usage/debug.ts +97 -0
  800. package/src/usage/expected-prices.ts +416 -0
  801. package/src/usage/log.ts +1223 -0
  802. package/src/usage/summary.ts +753 -0
  803. package/src/usage/totals.ts +14 -0
  804. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  805. package/src/usage/user-cost-overlays.ts +314 -0
  806. package/src/vision/anthropic-describe.ts +189 -0
  807. package/src/vision/backends.ts +97 -0
  808. package/src/vision/describe.ts +131 -0
  809. package/src/vision/eligibility.ts +250 -0
  810. package/src/vision/index.ts +681 -0
  811. package/src/vision/reasoning.ts +55 -0
  812. package/src/vision/routed-describe.ts +175 -0
  813. package/src/vision/timeout-bounds.ts +9 -0
  814. package/src/web-search/anthropic-executor.ts +195 -0
  815. package/src/web-search/backends.ts +108 -0
  816. package/src/web-search/exa-executor.ts +88 -0
  817. package/src/web-search/executor.ts +113 -0
  818. package/src/web-search/format-result.ts +89 -0
  819. package/src/web-search/gemini-executor.ts +141 -0
  820. package/src/web-search/index.ts +331 -0
  821. package/src/web-search/loop.ts +896 -0
  822. package/src/web-search/parse.ts +315 -0
  823. package/src/web-search/progress-stream.ts +342 -0
  824. package/src/web-search/sources.ts +60 -0
  825. package/src/web-search/synthetic-tool.ts +47 -0
  826. package/src/web-search/xai-executor.ts +219 -0
@@ -0,0 +1,1653 @@
1
+ import http2 from "node:http2";
2
+ import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
3
+ import { namespacedToolName, type OcxProviderConfig, type OcxUsage } from "../../types";
4
+ import { CONNECT_FLAG_END_STREAM, ConnectFrameError, consumeConnectFrames, encodeConnectFrame } from "./framing";
5
+ import {
6
+ CURSOR_MAX_EFFECTIVE_CONNECT_PAYLOAD_BYTES,
7
+ CURSOR_MAX_CONNECT_FRAME_BYTES,
8
+ CURSOR_MAX_PENDING_FRAMES,
9
+ CURSOR_PENDING_FRAMES_RESUME,
10
+ CURSOR_TRANSPORT_MAX_BUFFERED_BYTES,
11
+ CURSOR_TRANSPORT_RESUME_BYTES,
12
+ TranslatorBudgetExceededError,
13
+ type TranslatorBudget,
14
+ } from "../../lib/translator-budget";
15
+ import { activePromptText, prepareCursorRunRequest } from "./protobuf-request";
16
+ import { prepareCursorRawMessages, resolveActiveCursorImages } from "./images";
17
+ import { cursorRequestMessagesFromRaw } from "./request-builder";
18
+ import {
19
+ createCursorContextUsageTracker,
20
+ createCursorProtobufEventState,
21
+ finalizeTurnEvents,
22
+ mapCursorProtobufServerMessage,
23
+ mapSyntheticMcpExecToToolEvents,
24
+ reportableContextTokens,
25
+ resolvedTurnUsage,
26
+ usageFromContextTokens,
27
+ } from "./protobuf-events";
28
+ import {
29
+ AgentClientMessageSchema,
30
+ AgentServerMessageSchema,
31
+ AskQuestionInteractionResponseSchema,
32
+ AskQuestionRejectedSchema,
33
+ AskQuestionResultSchema,
34
+ ClientHeartbeatSchema,
35
+ CreatePlanRequestResponseSchema,
36
+ CreatePlanResultSchema,
37
+ CreatePlanSuccessSchema,
38
+ ConversationStateStructureSchema,
39
+ ExaFetchRequestResponseSchema,
40
+ ExaFetchRequestResponse_ApprovedSchema,
41
+ ExaSearchRequestResponseSchema,
42
+ ExaSearchRequestResponse_ApprovedSchema,
43
+ InteractionResponseSchema,
44
+ SwitchModeRequestResponseSchema,
45
+ SwitchModeRequestResponse_RejectedSchema,
46
+ WebSearchRequestResponseSchema,
47
+ WebSearchRequestResponse_ApprovedSchema,
48
+ type AgentServerMessage,
49
+ type ExecServerMessage,
50
+ type InteractionQuery,
51
+ type InteractionResponse,
52
+ } from "./gen/agent_pb";
53
+ import { debugProviderDiagnostic } from "../../lib/debug";
54
+ import { classifyCursorError, CursorUnexpectedCancelError, isCursorAbortError, isCursorBenignCancelError, safeCursorErrorMessage } from "./cursor-errors";
55
+ import { mcpArgsFromToolCall } from "./protobuf-events";
56
+ import { OCX_RESPONSES_TOOL_PROVIDER } from "./tool-definitions";
57
+ import {
58
+ handleCursorNativeExec,
59
+ handleCursorNativeKv,
60
+ releaseCursorBlobRequestScope,
61
+ type CursorBlobRequestScopeToken,
62
+ type CursorNativeExecContext,
63
+ } from "./native-exec";
64
+ import { effectiveCursorNativeExecAllow } from "./exec-policy";
65
+ import { resolveMcpServers } from "./mcp-config";
66
+ import { CursorMcpManager } from "./mcp-manager";
67
+ import { buildMcpToolDefinitions, mcpDepsFromManager } from "./native-exec-mcp";
68
+ import { desktopDepsFromConfig } from "./native-exec-desktop";
69
+ import {
70
+ buildCursorToolDefinitions,
71
+ cursorRequestAdvertisesApplyPatch,
72
+ cursorRequestHasShellAlias,
73
+ cursorToolArgNormalizeSchema,
74
+ cursorToolWireName,
75
+ cursorToolsForActivePrompt,
76
+ isCursorSyntheticStructuredEditTool,
77
+ isGenericToolUseCountDemoPrompt,
78
+ requestedCursorToolUseCount,
79
+ } from "./tool-definitions";
80
+ import type { CursorNativeToolDeps } from "./native-exec-tools";
81
+ import {
82
+ terminateBackgroundShellsForSession,
83
+ type BackgroundShellTerminationReport,
84
+ } from "./native-exec-shell";
85
+ import type { CursorClientMessage, CursorRunRequest, CursorServerMessage } from "./types";
86
+ import type { CursorTransport, CursorTransportFactoryInput } from "./transport";
87
+ import { CursorHttp1BidiConnection } from "./http1-bidi";
88
+ import { isPinnedHttp1 } from "../../lib/upstream-http-version";
89
+
90
+ const CURSOR_RUN_PATH = "/agent.v1.AgentService/Run";
91
+ const CURSOR_CLIENT_VERSION = "cli-2026.07.08-0c04a8a";
92
+ const HEARTBEAT_MS = 5_000;
93
+ const CURSOR_FIRST_FRAME_TIMEOUT_MS = 30_000;
94
+ /**
95
+ * T04 (senpi #1062 second half): after the first frame, a turn with NO inbound decoded
96
+ * frames for this long is failed instead of waiting for the 300s bridge stall watchdog
97
+ * (issue #2210). Reset on every decoded AgentServerMessage.
98
+ */
99
+ const CURSOR_STREAM_SILENCE_FAIL_MS = 30_000;
100
+ /**
101
+ * A stream that produces ONLY liveness frames (server heartbeat / conversationCheckpointUpdate)
102
+ * for this long is equally stuck — the server is alive but the turn is not progressing.
103
+ * Reset on every decoded frame that is not liveness-only.
104
+ */
105
+ const CURSOR_STREAM_HEARTBEAT_ONLY_FAIL_MS = 90_000;
106
+ /**
107
+ * After `turnEnded` is decoded, the application turn is complete. A server that keeps
108
+ * HTTP/2 open past this point cannot hold the turn hostage (senpi #1062): we close our side
109
+ * after a short grace so any trailing frames (late usage, checkpoint) still land.
110
+ */
111
+ const TURN_ENDED_CLOSE_GRACE_MS = 500;
112
+ const CURSOR_TIMEOUT_DESTROY_GRACE_MS = 1_000;
113
+ const CLIENT_TOOL_FINALIZE_GRACE_MS = 50;
114
+ const GENERIC_TOOL_COUNT_MIN_FINALIZE_GRACE_MS = 750;
115
+ const GENERIC_TOOL_COUNT_MAX_FINALIZE_GRACE_MS = 1_800;
116
+ const GENERIC_TOOL_COUNT_PER_TOOL_GRACE_MS = 125;
117
+ const cursorContextUsageTracker = createCursorContextUsageTracker();
118
+
119
+ /**
120
+ * Single-shot terminal settlement for one Cursor turn: whichever of fail/finish wins first owns
121
+ * the terminal; later calls are no-ops. Prevents double-terminal mutation when multiple sources
122
+ * race (stream error + session error, end + late session error, timeout + destroy error).
123
+ * Exported for direct unit testing — the callbacks are otherwise private to run()/open().
124
+ */
125
+ export function createTerminalSettler(hooks: {
126
+ fail: (error: Error) => void;
127
+ finish: () => void;
128
+ clearTimer: () => void;
129
+ }): { settleFail: (error: Error) => void; settleFinish: () => void; settled: () => boolean } {
130
+ let settled = false;
131
+ return {
132
+ settleFail(error) {
133
+ if (settled) return;
134
+ settled = true;
135
+ hooks.clearTimer();
136
+ hooks.fail(error);
137
+ },
138
+ settleFinish() {
139
+ if (settled) return;
140
+ settled = true;
141
+ hooks.clearTimer();
142
+ hooks.finish();
143
+ },
144
+ settled: () => settled,
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Arm the post-close destroy fallback for a timed-out turn: close() waits for in-flight frames,
150
+ * but a dead socket can ignore it, leaving a stalled TLS session past the timeout. Exported for
151
+ * unit testing with fakes. The timer is unref'd so it never holds the process open.
152
+ */
153
+ export function armTimeoutDestroyFallback(
154
+ stream: { destroyed: boolean; destroy: () => void },
155
+ session: { destroyed: boolean; destroy: () => void },
156
+ graceMs: number,
157
+ ): ReturnType<typeof setTimeout> {
158
+ const timer = setTimeout(() => {
159
+ try { if (!stream.destroyed) stream.destroy(); } catch { /* gone */ }
160
+ try { if (!session.destroyed) session.destroy(); } catch { /* gone */ }
161
+ }, graceMs);
162
+ timer.unref?.();
163
+ return timer;
164
+ }
165
+
166
+ /** Carry context-usage totals across conversation-id rotation for external-model replay. */
167
+ export function rekeyCursorContextUsage(fromConversationId: string, toConversationId: string): void {
168
+ cursorContextUsageTracker.rekey(fromConversationId, toConversationId);
169
+ }
170
+
171
+ export class CursorMissingCredentialError extends Error {
172
+ readonly code = "cursor_missing_credential";
173
+
174
+ constructor() {
175
+ super("Cursor live transport requires a Cursor access token in provider.apiKey, Authorization, or OPENCODEX_CURSOR_TEST_TOKEN.");
176
+ this.name = "CursorMissingCredentialError";
177
+ }
178
+ }
179
+
180
+ export function resolveCursorToken(provider: OcxProviderConfig, headers?: Headers): string {
181
+ const providerKey = provider.apiKey?.trim();
182
+ if (providerKey) return providerKey;
183
+
184
+ const forwarded = headers?.get("authorization") ?? headers?.get("Authorization");
185
+ if (forwarded?.toLowerCase().startsWith("bearer ")) return forwarded.slice("bearer ".length).trim();
186
+
187
+ const envToken = process.env.OPENCODEX_CURSOR_TEST_TOKEN?.trim();
188
+ if (envToken) return envToken;
189
+ throw new CursorMissingCredentialError();
190
+ }
191
+
192
+ /**
193
+ * Classify a Connect end-stream (trailer) frame. Cursor terminates EVERY stream with this
194
+ * frame; success is signalled by the ABSENCE of an `error` field (typically `{}`), not by the
195
+ * absence of the frame. Returns null on success, an Error only on a real Connect error.
196
+ * Mirrors jawcode `parseConnectEndStream` (see devlog 350.98). Exported for unit testing.
197
+ */
198
+ export function parseConnectEndStreamError(payload: Uint8Array): Error | null {
199
+ try {
200
+ const parsed = JSON.parse(new TextDecoder().decode(payload)) as { error?: { code?: string; message?: string } };
201
+ if (parsed?.error) {
202
+ return new Error(`Cursor Connect error ${parsed.error.code ?? "unknown"}: ${parsed.error.message ?? "Unknown error"}`);
203
+ }
204
+ return null;
205
+ } catch {
206
+ return new Error("Cursor Connect end-stream error");
207
+ }
208
+ }
209
+
210
+ function encodeClientMessage(message: Parameters<typeof create<typeof AgentClientMessageSchema>>[1]): Uint8Array {
211
+ return encodeConnectFrame(toBinary(AgentClientMessageSchema, create(AgentClientMessageSchema, message)));
212
+ }
213
+
214
+ /**
215
+ * Decide how to handle an `execServerMessage.mcpArgs` frame for a client (Responses-provider) tool.
216
+ *
217
+ * A stateless Responses proxy cannot send Cursor a real `mcpResult` later (Cursor's MCP exec is
218
+ * synchronous on the live h2 stream; there is no deferred-result signal). So when Cursor asks us to
219
+ * run a client Responses tool we must:
220
+ * 1. surface the tool call to Codex (tool_call_start/delta/end),
221
+ * 2. deliberately END turn 1 as `done`/completed — Cursor will never send `turnEnded` because it
222
+ * is waiting for an `mcpResult` that never comes, so relying on the stall watchdog would make
223
+ * turn 1 `response.incomplete` and drop the conversation id (continuation dies at step 1), and
224
+ * 3. cancel the Cursor run WITHOUT writing any fake `mcpResult`.
225
+ * The real tool result arrives on the NEXT /v1/responses request as structured history.
226
+ *
227
+ * Pure (no I/O) so the decision is unit-testable. `handleServerMessage` performs the side effects.
228
+ */
229
+ export interface McpArgsPlan {
230
+ handledByResponsesBridge: boolean;
231
+ events: CursorServerMessage[];
232
+ cancelCursorRun: boolean;
233
+ /**
234
+ * The Responses bridge owns this exec and every known client tool call is committed, but turn 1 is
235
+ * NOT ended synchronously: a sibling call may still be announced in a later receive chunk. The
236
+ * transport arms a revocable grace timer and only ends the turn (see finalizeAfterDrain) if the set
237
+ * is still drained when it fires.
238
+ */
239
+ finalizeWhenDrained: boolean;
240
+ writeMcpResult?: never;
241
+ }
242
+
243
+ export function planMcpArgsHandling(
244
+ execMsg: ExecServerMessage,
245
+ state: ReturnType<typeof createCursorProtobufEventState>,
246
+ ): McpArgsPlan {
247
+ if (execMsg.message.case !== "mcpArgs") {
248
+ return { handledByResponsesBridge: false, events: [], cancelCursorRun: false, finalizeWhenDrained: false };
249
+ }
250
+ const args = execMsg.message.value;
251
+ if (args.providerIdentifier !== OCX_RESPONSES_TOOL_PROVIDER) {
252
+ // A real MCP server tool: native exec handles it (executed locally, real mcpResult written).
253
+ return { handledByResponsesBridge: false, events: [], cancelCursorRun: false, finalizeWhenDrained: false };
254
+ }
255
+
256
+ // From here on the Responses bridge owns the exec: never fall through to native exec, which would
257
+ // send Cursor a bogus "bridge suspension not implemented" mcpResult error.
258
+ const toolEvents = mapSyntheticMcpExecToToolEvents(args, `exec_${execMsg.id}`, {
259
+ allowEmptyArgs: true,
260
+ state,
261
+ });
262
+
263
+ if (toolEvents.some(event => event.type === "error")) {
264
+ // The error is itself the terminal signal; do not also emit `done`.
265
+ return { handledByResponsesBridge: true, events: toolEvents, cancelCursorRun: true, finalizeWhenDrained: false };
266
+ }
267
+
268
+ // Parallel safety ("tool use N"): Cursor sends one exec mcpArgs per client tool call. An empty
269
+ // openToolCalls set proves only that every KNOWN call is committed, not that Cursor has finished
270
+ // announcing siblings — a sibling's toolCallStarted can still arrive in a later receive chunk. So
271
+ // never end turn 1 synchronously here: surface this call's events, and when the set is drained flag
272
+ // finalizeWhenDrained so the transport arms a revocable grace timer (finalizeAfterDrain re-checks
273
+ // the guard when it fires). While siblings are still open, just keep the stream open.
274
+ return {
275
+ handledByResponsesBridge: true,
276
+ events: toolEvents,
277
+ cancelCursorRun: false,
278
+ finalizeWhenDrained: state.openToolCalls.size === 0,
279
+ };
280
+ }
281
+
282
+ /**
283
+ * Build the `interactionResponse` reply for a server `interactionQuery`. Cursor's server-side agent
284
+ * BLOCKS on these queries until the client answers (matching `id`); an unanswered query is the
285
+ * proven cause of the heartbeat-only stall → watchdog `upstream_stall_timeout` → upstream 502 loop
286
+ * (devlog 260702_cursor-live-stability-rca). ocx is a headless non-interactive client, so:
287
+ * - createPlan: acknowledge success (the agent proceeds to execute); the plan text is surfaced to
288
+ * Codex as visible output so the user still sees it.
289
+ * - askQuestion: reject with a reason — the agent must proceed autonomously; there is no human to
290
+ * answer mid-turn. (Future: bridge to a Codex user-input request.)
291
+ * - webSearch / exaSearch / exaFetch: APPROVE (empty approval). These are approve/reject
292
+ * permission gates, not client-run requests — the response schema has no result field, so
293
+ * approval delegates the search to Cursor's SERVER, which runs it and injects results into the
294
+ * model server-side (the answer then streams back as textDelta; the display-plane
295
+ * web_search_tool_call/exa_*_tool_call result frames are native, non-mcp, and safely dropped by
296
+ * the event mapper). Rejecting them (the old default) killed the model's web capability on the
297
+ * Cursor path. Tradeoff: approval consumes the user's Cursor web-search/Exa quota. The synthetic
298
+ * web_search sidecar (src/web-search) is an orthogonal proxy-side path used only when the client
299
+ * sends a hosted web_search tool; it does not cover Cursor-native web search.
300
+ * - switchMode: reject (deterministic default; no non-interactive mode switch).
301
+ * - setupVmEnvironment: the result schema has no error case — reply success so the agent is not
302
+ * left waiting; the command itself was never run locally.
303
+ * Pure (no I/O) for unit testing; `handleServerMessage` writes the frame and emits liveness.
304
+ */
305
+ export function planInteractionQueryReply(query: InteractionQuery): { response: InteractionResponse; replyCase: string; planText?: string } {
306
+ const NON_INTERACTIVE_REASON = "opencodex bridge is non-interactive; proceed without this interaction.";
307
+ const q = query.query;
308
+ const respond = (result: InteractionResponse["result"]): InteractionResponse =>
309
+ create(InteractionResponseSchema, { id: query.id, result });
310
+
311
+ if (q.case === "createPlanRequestQuery") {
312
+ const args = q.value.args;
313
+ const parts = [
314
+ args?.name ? `Plan: ${args.name}` : undefined,
315
+ args?.overview?.trim() ? args.overview.trim() : undefined,
316
+ args?.plan?.trim() ? args.plan.trim() : undefined,
317
+ ].filter((part): part is string => typeof part === "string" && part.length > 0);
318
+ return {
319
+ response: respond({
320
+ case: "createPlanRequestResponse",
321
+ value: create(CreatePlanRequestResponseSchema, {
322
+ result: create(CreatePlanResultSchema, { result: { case: "success", value: create(CreatePlanSuccessSchema, {}) } }),
323
+ }),
324
+ }),
325
+ replyCase: "createPlanRequestResponse:success",
326
+ planText: parts.length > 0 ? `${parts.join("\n\n")}\n` : undefined,
327
+ };
328
+ }
329
+ if (q.case === "askQuestionInteractionQuery") {
330
+ return {
331
+ response: respond({
332
+ case: "askQuestionInteractionResponse",
333
+ value: create(AskQuestionInteractionResponseSchema, {
334
+ result: create(AskQuestionResultSchema, {
335
+ result: { case: "rejected", value: create(AskQuestionRejectedSchema, { reason: NON_INTERACTIVE_REASON }) },
336
+ }),
337
+ }),
338
+ }),
339
+ replyCase: "askQuestionInteractionResponse:rejected",
340
+ };
341
+ }
342
+ if (q.case === "switchModeRequestQuery") {
343
+ return {
344
+ response: respond({
345
+ case: "switchModeRequestResponse",
346
+ value: create(SwitchModeRequestResponseSchema, {
347
+ result: { case: "rejected", value: create(SwitchModeRequestResponse_RejectedSchema, { reason: NON_INTERACTIVE_REASON }) },
348
+ }),
349
+ }),
350
+ replyCase: "switchModeRequestResponse:rejected",
351
+ };
352
+ }
353
+ if (q.case === "webSearchRequestQuery") {
354
+ return {
355
+ response: respond({
356
+ case: "webSearchRequestResponse",
357
+ value: create(WebSearchRequestResponseSchema, {
358
+ result: { case: "approved", value: create(WebSearchRequestResponse_ApprovedSchema, {}) },
359
+ }),
360
+ }),
361
+ replyCase: "webSearchRequestResponse:approved",
362
+ };
363
+ }
364
+ if (q.case === "exaSearchRequestQuery") {
365
+ return {
366
+ response: respond({
367
+ case: "exaSearchRequestResponse",
368
+ value: create(ExaSearchRequestResponseSchema, {
369
+ result: { case: "approved", value: create(ExaSearchRequestResponse_ApprovedSchema, {}) },
370
+ }),
371
+ }),
372
+ replyCase: "exaSearchRequestResponse:approved",
373
+ };
374
+ }
375
+ if (q.case === "exaFetchRequestQuery") {
376
+ return {
377
+ response: respond({
378
+ case: "exaFetchRequestResponse",
379
+ value: create(ExaFetchRequestResponseSchema, {
380
+ result: { case: "approved", value: create(ExaFetchRequestResponse_ApprovedSchema, {}) },
381
+ }),
382
+ }),
383
+ replyCase: "exaFetchRequestResponse:approved",
384
+ };
385
+ }
386
+ if (q.case === "setupVmEnvironmentArgs") {
387
+ // setupVmEnvironment is not supported — reply with an empty InteractionResponse so the stream
388
+ // stays alive instead of throwing (which kills the entire gRPC connection via failAndClear).
389
+ return {
390
+ response: respond({ case: undefined, value: undefined }),
391
+ replyCase: "unsupported:setupVmEnvironment",
392
+ };
393
+ }
394
+ // Unknown interaction query case — Cursor added a new query type that our protobuf definition
395
+ // does not include yet. Gracefully reply with an empty InteractionResponse (matching id, no
396
+ // result) so the server unblocks and the stream stays alive. Previously this threw, which
397
+ // propagated through .catch → failAndClear and killed the entire connection (#116).
398
+ return {
399
+ response: respond({ case: undefined, value: undefined }),
400
+ replyCase: `unsupported:${q.case ?? "unknown"}`,
401
+ };
402
+ }
403
+
404
+ /**
405
+ * Re-check the drain guard at grace-timer fire time and finalize turn 1 only if still drained. A
406
+ * sibling client tool call announced after the timer was armed reopens `openToolCalls`, so this
407
+ * returns `[]` (the pending finalize is revoked); a later drain re-arms it. Pure for unit testing.
408
+ */
409
+ export function finalizeAfterDrain(state: ReturnType<typeof createCursorProtobufEventState>): CursorServerMessage[] {
410
+ if (state.terminated) return [];
411
+ if (state.openToolCalls.size > 0) return [];
412
+ return finalizeTurnEvents(state);
413
+ }
414
+
415
+ export function clientToolFinalizeGraceMsForRequest(request: CursorRunRequest, baseGraceMs = CLIENT_TOOL_FINALIZE_GRACE_MS): number {
416
+ if (request.rawMessages?.at(-1)?.role === "toolResult") return baseGraceMs;
417
+ const text = activePromptText(request);
418
+ if (!cursorRequestHasShellAlias(request.tools) || !isGenericToolUseCountDemoPrompt(text)) return baseGraceMs;
419
+ const requestedCount = requestedCursorToolUseCount(text);
420
+ const expandedGraceMs = requestedCount
421
+ ? Math.min(
422
+ GENERIC_TOOL_COUNT_MAX_FINALIZE_GRACE_MS,
423
+ Math.max(GENERIC_TOOL_COUNT_MIN_FINALIZE_GRACE_MS, requestedCount * GENERIC_TOOL_COUNT_PER_TOOL_GRACE_MS),
424
+ )
425
+ : GENERIC_TOOL_COUNT_MIN_FINALIZE_GRACE_MS;
426
+ return Math.max(baseGraceMs, expandedGraceMs);
427
+ }
428
+
429
+ class LiveCursorTransport implements CursorTransport {
430
+ private session?: http2.ClientHttp2Session;
431
+ private stream?: http2.ClientHttp2Stream;
432
+ private http1Connection?: CursorHttp1BidiConnection;
433
+ private heartbeat?: ReturnType<typeof setInterval>;
434
+ private firstFrameTimer?: ReturnType<typeof setTimeout>;
435
+ private turnEndedCloseTimer?: ReturnType<typeof setTimeout>;
436
+ /**
437
+ * T04 inbound stream-health watchdog. Armed after the request is on the wire, reset by
438
+ * every DECODED frame (raw chunks deliberately do not count — TLS keepalive noise must not
439
+ * defeat it), disarmed by any settle/expected-close path. One timer covers both thresholds:
440
+ * it always fires at min(lastInbound + silence, lastMeaningful + heartbeatOnly) and re-arms
441
+ * when neither deadline has actually elapsed.
442
+ */
443
+ private streamHealthTimer?: ReturnType<typeof setTimeout>;
444
+ private lastInboundFrameAt = 0;
445
+ private lastMeaningfulFrameAt = 0;
446
+ private streamHealthFail?: (error: Error) => void;
447
+ private committed = false;
448
+ private expectedClose = false;
449
+ /**
450
+ * True once a terminal (`done` or `error`) has been admitted to the outbound queue. Read only
451
+ * by the EOF branch below: after a mapper error the bridge has already failed the turn, so
452
+ * failing again on EOF would add a duplicate adapter error for no benefit.
453
+ */
454
+ private emittedTerminal = false;
455
+ private pendingFinalize?: ReturnType<typeof setTimeout>;
456
+ private readonly clientToolFinalizeGraceMs: number;
457
+ private activeClientToolFinalizeGraceMs: number;
458
+ private readonly token: string;
459
+ private readonly mcpManager?: CursorMcpManager;
460
+ private readonly translatorBudget: TranslatorBudget;
461
+ private pendingTransportFrames = 0;
462
+ private transportBufferedBytes = 0;
463
+ private readonly desktopDeps: CursorNativeToolDeps;
464
+ private execContext: CursorNativeExecContext = {};
465
+ private mcpPrepared?: Promise<void>;
466
+ private releaseMcpObservation?: () => void;
467
+ private blobRequestScope?: CursorBlobRequestScopeToken;
468
+ private shellCleanup?: Promise<BackgroundShellTerminationReport>;
469
+ // Per-turn diagnostic counters/timestamps when provider debug is on (`ocx debug provider on`). Stamped in open(), cleared on
470
+ // close; safe to read after a stream failure because open() owns the only writer before run().
471
+ private turnStartedAt = 0;
472
+ private framesReceived = 0;
473
+ private sawAssistantText = false;
474
+ private firstFrameAt?: number;
475
+ private firstFrameLogged = false;
476
+ /** Stable session identifier sent as x-session-id; mirrors IDE session semantics. */
477
+ private readonly sessionId: string;
478
+ /** Per-transport owner for native-exec / background shells. Must not share conversationId. */
479
+ private readonly shellOwnerId = crypto.randomUUID();
480
+ private capturedCheckpointBytes?: Uint8Array;
481
+
482
+ constructor(private readonly input: CursorTransportFactoryInput) {
483
+ this.sessionId = input.sessionId?.trim() || crypto.randomUUID();
484
+ this.translatorBudget = input.translatorBudget;
485
+ this.token = resolveCursorToken(input.provider, input.headers);
486
+ // Grace window before a drained client-tool turn is finalized. Small enough not to look like a
487
+ // stall, large enough to catch a sibling tool call announced in the next receive chunk. Injectable
488
+ // so the transport-level race test can drive it deterministically.
489
+ this.clientToolFinalizeGraceMs = input.clientToolFinalizeGraceMs ?? CLIENT_TOOL_FINALIZE_GRACE_MS;
490
+ this.activeClientToolFinalizeGraceMs = this.clientToolFinalizeGraceMs;
491
+ // Desktop (computer-use / record-screen) executors are available even with no MCP servers.
492
+ this.desktopDeps = desktopDepsFromConfig(input.provider.desktopExecutor);
493
+ this.execContext = {
494
+ ...this.desktopDeps,
495
+ sessionId: this.shellOwnerId,
496
+ unsafeAllowNativeLocalExec: effectiveCursorNativeExecAllow(input.provider, input.requestDeclaresFullAccess === true),
497
+ };
498
+ const servers = resolveMcpServers(input.provider);
499
+ if (servers.length > 0) {
500
+ this.mcpManager = new CursorMcpManager(servers, {
501
+ log: message => console.warn(message),
502
+ maxTools: input.provider.mcpMaxTools,
503
+ maxSchemaBytes: input.provider.mcpMaxSchemaBytes,
504
+ maxResultBytes: input.provider.mcpMaxResultBytes,
505
+ });
506
+ }
507
+ }
508
+
509
+ /**
510
+ * Connect MCP servers and compute the tool definitions advertised to the Cursor server.
511
+ * MUST complete before the first `requestContextArgs` (the server only calls MCP tools it was
512
+ * told about), so `run()` awaits this before opening the stream. Preparation failures reject the
513
+ * turn instead of silently running with MCP disabled.
514
+ */
515
+ private prepareMcp(): Promise<void> {
516
+ if (!this.mcpManager) return Promise.resolve();
517
+ if (!this.mcpPrepared) {
518
+ this.mcpPrepared = (async () => {
519
+ try {
520
+ const mcpToolDefs = await buildMcpToolDefinitions(this.mcpManager!);
521
+ this.releaseMcpObservation?.();
522
+ this.releaseMcpObservation = this.translatorBudget.observeExternallyCapped(
523
+ "mcp_payload",
524
+ new TextEncoder().encode(JSON.stringify(mcpToolDefs)).byteLength,
525
+ );
526
+ this.execContext = {
527
+ ...this.desktopDeps,
528
+ ...mcpDepsFromManager(this.mcpManager!),
529
+ mcpToolDefs,
530
+ sessionId: this.shellOwnerId,
531
+ unsafeAllowNativeLocalExec: effectiveCursorNativeExecAllow(this.input.provider, this.input.requestDeclaresFullAccess === true),
532
+ };
533
+ } catch (err) {
534
+ throw new Error(`Cursor MCP preparation failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
535
+ }
536
+ })();
537
+ }
538
+ return this.mcpPrepared;
539
+ }
540
+
541
+ toJSON(): Record<string, string> {
542
+ return { type: "LiveCursorTransport", credential: "redacted" };
543
+ }
544
+
545
+ async *run(request: CursorRunRequest, signal?: AbortSignal): AsyncIterable<CursorServerMessage> {
546
+ const queue: Array<{ message: CursorServerMessage; bytes: number }> = [];
547
+ let notify: (() => void) | undefined;
548
+ let done = false;
549
+ let failure: Error | undefined;
550
+ let state = createCursorProtobufEventState({ translatorBudget: this.translatorBudget });
551
+ let failureLogged = false;
552
+ // One per-turn summary of the failure path (end-stream error, socket reset, abort) so the
553
+ // operator can see how far the turn got and how it was classified without re-scanning every
554
+ // frame. Gated behind provider debug (`ocx debug provider on`).
555
+ const summarizeFailure = (err: Error): Error => {
556
+ if (!failureLogged && !(this.expectedClose && isCursorBenignCancelError(err))) {
557
+ failureLogged = true;
558
+ debugProviderDiagnostic("cursor", "turn-failed", {
559
+ committed: this.committed,
560
+ framesReceived: this.framesReceived,
561
+ outputTokens: state.usage.outputTokens,
562
+ contextTokens: state.contextTokens,
563
+ firstFrameMs: this.firstFrameAt ? this.firstFrameAt - this.turnStartedAt : undefined,
564
+ elapsedMs: this.turnStartedAt ? Date.now() - this.turnStartedAt : undefined,
565
+ classified: classifyCursorError(err.message),
566
+ errorCode: (err as { code?: unknown }).code ?? undefined,
567
+ message: redactCursorForLog(err.message),
568
+ });
569
+ }
570
+ return err;
571
+ };
572
+ /**
573
+ * A cancel we did not request is a real transport failure, but as a raw `NGHTTP2_CANCEL` it
574
+ * gets swallowed twice over: the adapter re-decides "benign" from the error code alone
575
+ * (`cursor.ts:181`) and drops the turn, and any message that survives is re-matched
576
+ * downstream and labelled an intentional "Cursor stream suspended". Raising a typed error
577
+ * carries the provenance this class already holds.
578
+ *
579
+ * Suppressed once a terminal was emitted: the turn already ended, and a second terminal flips
580
+ * a completed buffered response to failed.
581
+ */
582
+ const classifyTurnFailure = (err: Error): Error => {
583
+ if (!this.expectedClose && !this.emittedTerminal && isCursorBenignCancelError(err)) {
584
+ return summarizeFailure(new CursorUnexpectedCancelError(err));
585
+ }
586
+ return summarizeFailure(err);
587
+ };
588
+ const wake = () => {
589
+ const fn = notify;
590
+ notify = undefined;
591
+ fn?.();
592
+ };
593
+
594
+ const push = (message: CursorServerMessage) => {
595
+ const bytes = new TextEncoder().encode(JSON.stringify(message)).byteLength;
596
+ this.reserveTransportBytes(bytes);
597
+ if (message.type === "done" || message.type === "error") this.emittedTerminal = true;
598
+ queue.push({ message, bytes });
599
+ wake();
600
+ };
601
+
602
+ // Advertise MCP tools before the stream opens — the server only calls tools it was told about.
603
+ await this.prepareMcp();
604
+ // JPEG soft-cap rewrite for active-turn data: images before encode. Rebuild text
605
+ // messages from the prepared raw channel so omission markers replace stale
606
+ // pre-rewrite content that activePromptText and the tool filter would otherwise see.
607
+ const preparedRaw = await prepareCursorRawMessages(request.rawMessages, signal);
608
+ const preparedRawMessages = preparedRaw.messages;
609
+ const selectedImages = await resolveActiveCursorImages(
610
+ preparedRawMessages,
611
+ signal,
612
+ preparedRaw.images,
613
+ );
614
+ const preparedMessages = preparedRawMessages === request.rawMessages
615
+ ? request.messages
616
+ : cursorRequestMessagesFromRaw(preparedRawMessages);
617
+ const activeRequest: CursorRunRequest = {
618
+ ...request,
619
+ messages: preparedMessages,
620
+ rawMessages: preparedRawMessages,
621
+ selectedImages,
622
+ };
623
+ const activeText = activePromptText(activeRequest);
624
+ this.activeClientToolFinalizeGraceMs = clientToolFinalizeGraceMsForRequest(activeRequest, this.clientToolFinalizeGraceMs);
625
+ const cursorVisibleTools = cursorToolsForActivePrompt(activeRequest.tools, activeText, activeRequest.toolChoice);
626
+ const clientToolDefs = buildCursorToolDefinitions(cursorVisibleTools, activeRequest.toolChoice);
627
+ // `request.tools` is the catalog already filtered and budgeted by request-builder. Derive
628
+ // conversion provenance only from tagged synthetic tools that also survive this final prompt
629
+ // filter; a client tool with the same wire name can never opt into conversion by collision.
630
+ const syntheticStructuredEditToolNames = new Set(
631
+ (cursorVisibleTools ?? [])
632
+ .filter(isCursorSyntheticStructuredEditTool)
633
+ .map(cursorToolWireName),
634
+ );
635
+ const freeformToolNames = new Set(
636
+ (cursorVisibleTools ?? [])
637
+ .filter(tool => tool.freeform)
638
+ .map(tool => namespacedToolName(tool.namespace, tool.name)),
639
+ );
640
+ this.execContext = {
641
+ ...this.execContext,
642
+ clientToolDefs,
643
+ rejectNativeFileMutations: cursorRequestAdvertisesApplyPatch(request.tools, request.toolChoice),
644
+ structuredEditAvailable: syntheticStructuredEditToolNames.size > 0,
645
+ };
646
+ const toolSchemas = new Map<string, unknown>();
647
+ const cursorToolNameMap = new Map<string, string>();
648
+ for (const tool of cursorVisibleTools ?? []) {
649
+ const cursorWireName = cursorToolWireName(tool);
650
+ // Normalize against Responses/Codex field names, not the Cursor advertisement schema.
651
+ // Advertising `cmd` while also storing that schema here left `cmd` unmapped and Codex
652
+ // rejected shell_command with "missing field `command`" (#399).
653
+ toolSchemas.set(cursorWireName, cursorToolArgNormalizeSchema(tool));
654
+ cursorToolNameMap.set(cursorWireName, namespacedToolName(tool.namespace, tool.name));
655
+ }
656
+ const contextUsage = cursorContextUsageTracker.controlsForConversation(request.conversationId, {
657
+ clearPrior: request.contextUsageReset === true,
658
+ storeCheckpoints: request.contextUsageStoreCheckpoints !== false,
659
+ });
660
+ // Build the payload once. The estimate is only worth deriving when there is no
661
+ // carry-forward to fall back on — with a carry present it would never be used (#373).
662
+ const prepared = prepareCursorRunRequest(activeRequest, {
663
+ estimateInputTokens: contextUsage.carryForwardTokens === undefined,
664
+ });
665
+ this.blobRequestScope = prepared.blobRequestScope;
666
+ try {
667
+ state = createCursorProtobufEventState({
668
+ clientToolNames: clientToolDefs.map(tool => tool.toolName || tool.name),
669
+ freeformToolNames,
670
+ parallelToolCalls: request.parallelToolCalls,
671
+ toolSchemas,
672
+ cursorToolNameMap,
673
+ syntheticStructuredEditToolNames,
674
+ translatorBudget: this.translatorBudget,
675
+ contextUsage,
676
+ ...(prepared.estimatedInputTokens !== undefined
677
+ ? { estimatedInputTokens: prepared.estimatedInputTokens }
678
+ : {}),
679
+ });
680
+ this.open(prepared.bytes, signal, state, push, err => {
681
+ this.releaseBlobRequestScope();
682
+ failure = err;
683
+ wake();
684
+ }, () => {
685
+ this.releaseBlobRequestScope();
686
+ done = true;
687
+ wake();
688
+ });
689
+ } catch (error) {
690
+ this.releaseBlobRequestScope();
691
+ throw error;
692
+ }
693
+
694
+ while (!done || queue.length > 0) {
695
+ while (queue.length > 0) {
696
+ const queued = queue.shift();
697
+ if (queued) {
698
+ this.releaseTransportBytes(queued.bytes);
699
+ yield queued.message;
700
+ }
701
+ }
702
+ if (failure) {
703
+ // A CANCEL is benign only on the client-tool suspend path (expectedClose); an
704
+ // unexpected server-side NGHTTP2_CANCEL must surface as a real transport error.
705
+ if (this.expectedClose && isCursorBenignCancelError(failure)) return;
706
+ // A teardown error arriving AFTER the turn's terminal frame describes the connection,
707
+ // not the turn: the answer is committed and every queued message has been yielded.
708
+ //
709
+ // Narrow on purpose. A benign cancel after a terminal is already swallowed one layer
710
+ // up (`cursor.ts:183`), so widening this to every post-terminal error would change
711
+ // what the adapter sees for genuine faults. What it does cover is the abort case
712
+ // from #1527: `signal.abort` fires `failAndClear(new Error("Cursor request was
713
+ // aborted"))`, which is NOT benign (`cursor-errors.ts:74`), so an ordinary completed
714
+ // turn that is then torn down still surfaced as `turn-failed` with
715
+ // `expectedClose:false`. Only `cancelCursorRun()` sets `expectedClose`, so a normal
716
+ // completion never qualified for the branch above.
717
+ if (this.emittedTerminal && isCursorAbortError(failure)) return;
718
+ throw attachPartialUsage(classifyTurnFailure(failure), state);
719
+ }
720
+ if (done) break;
721
+ await new Promise<void>(resolve => {
722
+ notify = resolve;
723
+ });
724
+ }
725
+ if (failure) {
726
+ if (this.expectedClose && isCursorBenignCancelError(failure)) return;
727
+ if (this.emittedTerminal && isCursorAbortError(failure)) return;
728
+ throw attachPartialUsage(classifyTurnFailure(failure), state);
729
+ }
730
+ }
731
+
732
+ writeClient(_message: CursorClientMessage): void {}
733
+
734
+ private reserveTransportBytes(bytes: number): void {
735
+ if (this.transportBufferedBytes + bytes > CURSOR_TRANSPORT_MAX_BUFFERED_BYTES) {
736
+ throw new TranslatorBudgetExceededError("cursor_transport", CURSOR_TRANSPORT_MAX_BUFFERED_BYTES);
737
+ }
738
+ this.translatorBudget.chargeRetained(bytes, { kind: "cursor_transport" });
739
+ this.transportBufferedBytes += bytes;
740
+ this.updateTransportFlowControl();
741
+ }
742
+
743
+ private releaseTransportBytes(bytes: number): void {
744
+ this.transportBufferedBytes = Math.max(0, this.transportBufferedBytes - bytes);
745
+ this.translatorBudget.releaseRetained(bytes, { kind: "cursor_transport" });
746
+ this.updateTransportFlowControl();
747
+ }
748
+
749
+ private updateTransportFlowControl(): void {
750
+ if (
751
+ this.transportBufferedBytes >= CURSOR_TRANSPORT_MAX_BUFFERED_BYTES
752
+ || this.pendingTransportFrames >= CURSOR_MAX_PENDING_FRAMES
753
+ ) {
754
+ this.stream?.pause();
755
+ this.http1Connection?.pause();
756
+ return;
757
+ }
758
+ if (
759
+ this.transportBufferedBytes <= CURSOR_TRANSPORT_RESUME_BYTES
760
+ && this.pendingTransportFrames <= CURSOR_PENDING_FRAMES_RESUME
761
+ ) {
762
+ this.stream?.resume();
763
+ this.http1Connection?.resume();
764
+ }
765
+ }
766
+
767
+ private writeConnectFrame(frame: Uint8Array): void {
768
+ if (this.http1Connection) {
769
+ this.http1Connection.write(frame);
770
+ return;
771
+ }
772
+ this.stream?.write(frame);
773
+ }
774
+
775
+ requestCommitted(): boolean {
776
+ return this.committed;
777
+ }
778
+
779
+ private clearFirstFrameTimer(): void {
780
+ if (this.firstFrameTimer) {
781
+ clearTimeout(this.firstFrameTimer);
782
+ this.firstFrameTimer = undefined;
783
+ }
784
+ }
785
+
786
+ private clearStreamHealthTimer(): void {
787
+ if (this.streamHealthTimer) {
788
+ clearTimeout(this.streamHealthTimer);
789
+ this.streamHealthTimer = undefined;
790
+ }
791
+ this.streamHealthFail = undefined;
792
+ }
793
+
794
+ /**
795
+ * T04: arm (or re-arm) the inbound stream-health watchdog. `fail` is the turn's
796
+ * failAndClear; the timer owns nothing else. Never armed before the first decoded
797
+ * frame (the first-frame timer covers dial + first response), and disarmed by
798
+ * every settle / expected-close path alongside the other timers.
799
+ */
800
+ private armStreamHealthTimer(fail: (error: Error) => void): void {
801
+ if (this.streamHealthTimer) clearTimeout(this.streamHealthTimer);
802
+ if (this.expectedClose) return;
803
+ this.streamHealthFail = fail;
804
+ const silenceMs = this.input.streamSilenceFailMs ?? CURSOR_STREAM_SILENCE_FAIL_MS;
805
+ const heartbeatOnlyMs = this.input.streamHeartbeatOnlyFailMs ?? CURSOR_STREAM_HEARTBEAT_ONLY_FAIL_MS;
806
+ const now = Date.now();
807
+ const deadline = Math.min(
808
+ this.lastInboundFrameAt + silenceMs,
809
+ this.lastMeaningfulFrameAt + heartbeatOnlyMs,
810
+ );
811
+ this.streamHealthTimer = setTimeout(() => {
812
+ this.streamHealthTimer = undefined;
813
+ const failFn = this.streamHealthFail;
814
+ if (!failFn || this.expectedClose) return;
815
+ const stalledFor = Date.now() - this.lastInboundFrameAt;
816
+ const meaningfulStalledFor = Date.now() - this.lastMeaningfulFrameAt;
817
+ if (stalledFor < silenceMs && meaningfulStalledFor < heartbeatOnlyMs) {
818
+ // A frame landed between arming and firing — re-arm for the fresh deadline.
819
+ this.armStreamHealthTimer(failFn);
820
+ return;
821
+ }
822
+ const heartbeatOnly = stalledFor < silenceMs;
823
+ debugProviderDiagnostic("cursor", "stream-health-timeout", {
824
+ stalledMs: stalledFor,
825
+ meaningfulStalledMs: meaningfulStalledFor,
826
+ heartbeatOnly,
827
+ framesReceived: this.framesReceived,
828
+ elapsedMs: Date.now() - this.turnStartedAt,
829
+ });
830
+ const reason = heartbeatOnly
831
+ ? `Cursor stream stalled: heartbeat-only traffic for ${Math.round(meaningfulStalledFor / 1000)}s without turn progress`
832
+ : `Cursor stream stalled: no inbound frames for ${Math.round(stalledFor / 1000)}s before turnEnded`;
833
+ failFn(new Error(reason));
834
+ try { this.stream?.close(); } catch { this.stream?.destroy(); }
835
+ this.session?.close();
836
+ this.http1Connection?.close();
837
+ }, Math.max(0, deadline - now));
838
+ }
839
+
840
+ /**
841
+ * T04: record a decoded inbound frame. Liveness-only frames (server heartbeat,
842
+ * conversationCheckpointUpdate) keep the silence clock fresh but not the progress
843
+ * clock — matching senpi's split so a server that only pings still fails at the
844
+ * heartbeat-only threshold.
845
+ */
846
+ private noteInboundFrame(livenessOnly: boolean): void {
847
+ const now = Date.now();
848
+ this.lastInboundFrameAt = now;
849
+ if (!livenessOnly) this.lastMeaningfulFrameAt = now;
850
+ if (this.streamHealthFail) this.armStreamHealthTimer(this.streamHealthFail);
851
+ }
852
+
853
+ /**
854
+ * A clean Connect END_STREAM owns the turn terminal even when Cursor keeps the
855
+ * HTTP body open or tears it down with an abort/reset immediately afterward.
856
+ * Stop client-side liveness work and classify that later transport close as
857
+ * expected without actively sending an RST_STREAM back to Cursor.
858
+ */
859
+ private markProtocolComplete(): void {
860
+ this.expectedClose = true;
861
+ this.clearPendingFinalize();
862
+ if (this.heartbeat) {
863
+ clearInterval(this.heartbeat);
864
+ this.heartbeat = undefined;
865
+ }
866
+ this.clearFirstFrameTimer();
867
+ this.clearStreamHealthTimer();
868
+ }
869
+
870
+ private startShellCleanup(): Promise<BackgroundShellTerminationReport> {
871
+ return this.shellCleanup ??= terminateBackgroundShellsForSession(this.shellOwnerId);
872
+ }
873
+
874
+ async close(): Promise<void> {
875
+ if (this.heartbeat) clearInterval(this.heartbeat);
876
+ if (this.turnEndedCloseTimer) clearTimeout(this.turnEndedCloseTimer);
877
+ this.clearPendingFinalize();
878
+ this.clearFirstFrameTimer();
879
+ this.clearStreamHealthTimer();
880
+ this.stream?.close();
881
+ this.session?.close();
882
+ this.http1Connection?.close();
883
+ this.releaseBlobRequestScope();
884
+ this.releaseMcpObservation?.();
885
+ this.releaseMcpObservation = undefined;
886
+ void this.mcpManager?.dispose();
887
+ await this.startShellCleanup();
888
+ }
889
+
890
+ private cancelCursorRun(): void {
891
+ this.expectedClose = true;
892
+ this.clearPendingFinalize();
893
+ if (this.heartbeat) clearInterval(this.heartbeat);
894
+ this.clearFirstFrameTimer();
895
+ this.clearStreamHealthTimer();
896
+ if (this.http1Connection) {
897
+ this.http1Connection.close();
898
+ } else {
899
+ try {
900
+ this.stream?.close(http2.constants.NGHTTP2_CANCEL);
901
+ } catch {
902
+ this.stream?.destroy();
903
+ }
904
+ this.session?.close();
905
+ }
906
+ this.releaseBlobRequestScope();
907
+ this.releaseMcpObservation?.();
908
+ this.releaseMcpObservation = undefined;
909
+ void this.mcpManager?.dispose();
910
+ void this.startShellCleanup().catch(() => { /* close() observes the same cleanup promise */ });
911
+ }
912
+
913
+ /**
914
+ * T03 (#1062): after the server sends `turnEnded`, the application turn is complete.
915
+ * A server that keeps the HTTP/2 stream open past this point cannot hold the turn
916
+ * hostage until a 300s bridge idle timeout. Close our side after a short grace so any
917
+ * trailing frames (late usage, checkpoint) still land before we release the socket.
918
+ */
919
+ private closeAfterTurnEnded(): void {
920
+ if (this.turnEndedCloseTimer) return;
921
+ // The application turn is over: the T03 grace timer owns the socket from here.
922
+ // The T04 watchdog must disarm NOW, not at the grace close — a watchdog shorter
923
+ // than the grace would otherwise fail a completed turn.
924
+ this.clearStreamHealthTimer();
925
+ this.turnEndedCloseTimer = setTimeout(() => {
926
+ this.turnEndedCloseTimer = undefined;
927
+ // Only expectedClose (client-tool suspend cancel) blocks the close.
928
+ // emittedTerminal is intentionally NOT checked here: finalizeTurnEvents sets it
929
+ // synchronously during turnEnded mapping, ~500ms before this timer fires, so
930
+ // checking it would make the close unreachable on every real path (the exact
931
+ // scenario this PR exists to fix — senpi #1062).
932
+ if (this.expectedClose) return;
933
+ debugProviderDiagnostic("cursor", "turn-ended-close", {
934
+ committed: this.committed,
935
+ framesReceived: this.framesReceived,
936
+ });
937
+ this.expectedClose = true;
938
+ this.clearFirstFrameTimer();
939
+ this.clearStreamHealthTimer();
940
+ if (this.heartbeat) clearInterval(this.heartbeat);
941
+ if (this.http1Connection) {
942
+ this.http1Connection.close();
943
+ } else {
944
+ try {
945
+ this.stream?.close();
946
+ } catch {
947
+ this.stream?.destroy();
948
+ }
949
+ }
950
+ }, TURN_ENDED_CLOSE_GRACE_MS);
951
+ }
952
+
953
+ private releaseBlobRequestScope(): void {
954
+ const scope = this.blobRequestScope;
955
+ if (!scope) return;
956
+ this.blobRequestScope = undefined;
957
+ releaseCursorBlobRequestScope(scope);
958
+ }
959
+
960
+ private clearPendingFinalize(): void {
961
+ if (this.pendingFinalize) {
962
+ clearTimeout(this.pendingFinalize);
963
+ this.pendingFinalize = undefined;
964
+ }
965
+ }
966
+
967
+ /**
968
+ * Any frame that records or commits a client tool call revokes a pending finalize: the call set is
969
+ * about to change, so the drain that armed the timer is no longer authoritative. The timer re-arms
970
+ * when the set drains again (see scheduleClientToolFinalize).
971
+ */
972
+ private noteClientToolActivity(): void {
973
+ this.clearPendingFinalize();
974
+ }
975
+
976
+ /**
977
+ * Arm the revocable grace timer that ends a drained client-tool turn. On fire it re-checks the
978
+ * drain guard (finalizeAfterDrain): a sibling announced during the window reopened the set, so it
979
+ * emits nothing and waits for the next drain; otherwise it pushes the terminal `done` and cancels
980
+ * the Cursor run with RST_STREAM. No fake mcpResult is ever written.
981
+ */
982
+ private scheduleClientToolFinalize(
983
+ state: ReturnType<typeof createCursorProtobufEventState>,
984
+ push: (message: CursorServerMessage) => void,
985
+ ): void {
986
+ this.clearPendingFinalize();
987
+ this.pendingFinalize = setTimeout(() => {
988
+ this.pendingFinalize = undefined;
989
+ if (this.expectedClose) return;
990
+ const terminal = finalizeAfterDrain(state);
991
+ if (terminal.length === 0) return;
992
+ for (const event of terminal) push(event);
993
+ debugProviderDiagnostic("cursor", "client-tool-suspend", {
994
+ reason: "Responses bridge owns client tools; ending turn without fake mcpResult",
995
+ framesReceived: this.framesReceived,
996
+ elapsedMs: Date.now() - this.turnStartedAt,
997
+ });
998
+ this.cancelCursorRun();
999
+ }, this.activeClientToolFinalizeGraceMs);
1000
+ }
1001
+
1002
+ private open(
1003
+ encodedRequest: Uint8Array,
1004
+ signal: AbortSignal | undefined,
1005
+ state: ReturnType<typeof createCursorProtobufEventState>,
1006
+ push: (message: CursorServerMessage) => void,
1007
+ fail: (error: Error) => void,
1008
+ finish: () => void,
1009
+ ): void {
1010
+ if (signal?.aborted) {
1011
+ fail(signal.reason instanceof Error ? signal.reason : new Error("Cursor request was aborted"));
1012
+ return;
1013
+ }
1014
+ this.turnStartedAt = Date.now();
1015
+ this.framesReceived = 0;
1016
+ this.sawAssistantText = false;
1017
+ this.emittedTerminal = false;
1018
+ this.firstFrameAt = undefined;
1019
+ this.firstFrameLogged = false;
1020
+ const baseUrl = this.input.provider.baseUrl || "https://api2.cursor.sh";
1021
+ const useHttp1 = isPinnedHttp1(this.input.provider.upstreamHttpVersion);
1022
+ const requestId = crypto.randomUUID();
1023
+ const dialHost = cursorHostLabel(baseUrl);
1024
+ debugProviderDiagnostic("cursor", "dial", { host: dialHost, transport: useHttp1 ? "http1.1" : "http2" });
1025
+
1026
+ let session: http2.ClientHttp2Session | undefined;
1027
+ let stream: http2.ClientHttp2Stream | undefined;
1028
+ if (!useHttp1) {
1029
+ session = http2.connect(baseUrl);
1030
+ this.session = session;
1031
+ // The run request is buffered until the HTTP/2 session connects. Failures before `connect`
1032
+ // (DNS, ECONNREFUSED, TLS, connect timeout) mean the server never received the request, so they
1033
+ // are safe to retry. Once connected, bytes flush to the server and the turn must not be replayed.
1034
+ session.on("connect", () => {
1035
+ this.committed = true;
1036
+ debugProviderDiagnostic("cursor", "connected", {
1037
+ transport: "http2",
1038
+ connectMs: Date.now() - this.turnStartedAt,
1039
+ });
1040
+ });
1041
+ stream = session.request({
1042
+ ":method": "POST",
1043
+ ":path": CURSOR_RUN_PATH,
1044
+ "content-type": "application/connect+proto",
1045
+ "connect-protocol-version": "1",
1046
+ te: "trailers",
1047
+ authorization: `Bearer ${this.token}`,
1048
+ "x-ghost-mode": "true",
1049
+ "x-cursor-client-version": CURSOR_CLIENT_VERSION,
1050
+ "x-cursor-client-type": "cli",
1051
+ "x-request-id": requestId,
1052
+ "x-session-id": this.sessionId,
1053
+ });
1054
+ this.stream = stream;
1055
+ }
1056
+
1057
+ // Single-shot terminal owner for this turn (createTerminalSettler): stream error, session
1058
+ // error, trailers, end, abort, and the first-frame timeout all race into it, and only the
1059
+ // first wins. The first-frame timer is cleared by any settlement so it can never leak.
1060
+ const settler = createTerminalSettler({
1061
+ fail,
1062
+ finish,
1063
+ clearTimer: () => {
1064
+ this.clearFirstFrameTimer();
1065
+ this.clearStreamHealthTimer();
1066
+ },
1067
+ });
1068
+ const failAndClear = (error: Error) => {
1069
+ releaseBacklogLease();
1070
+ if (this.expectedClose) {
1071
+ // We already emitted a terminal `done` and cancelled the run (client-tool suspension). The
1072
+ // RST_STREAM CANCEL surfaces here as a stream error/abort; it is expected, not a failure.
1073
+ debugProviderDiagnostic("cursor", "stream-cancel-expected", {
1074
+ code: (error as { code?: unknown }).code,
1075
+ message: redactCursorForLog(error.message),
1076
+ framesReceived: this.framesReceived,
1077
+ elapsedMs: Date.now() - this.turnStartedAt,
1078
+ });
1079
+ settler.settleFinish();
1080
+ return;
1081
+ }
1082
+ settler.settleFail(error);
1083
+ };
1084
+ // Session-level errors (TLS/socket/GOAWAY) do not always propagate to the stream listener;
1085
+ // without this handler they could bypass orderly failure reporting entirely.
1086
+ const onSessionError = (err: unknown) => {
1087
+ const realErr = err instanceof Error ? err : new Error(String(err));
1088
+ debugProviderDiagnostic("cursor", "session-error", {
1089
+ code: String((realErr as { code?: unknown }).code ?? ""),
1090
+ message: redactCursorForLog(realErr.message),
1091
+ elapsedMs: Date.now() - this.turnStartedAt,
1092
+ });
1093
+ failAndClear(realErr);
1094
+ };
1095
+ this.firstFrameTimer = setTimeout(() => {
1096
+ this.firstFrameTimer = undefined;
1097
+ debugProviderDiagnostic("cursor", "first-frame-timeout", { timeoutMs: this.input.firstFrameTimeoutMs ?? CURSOR_FIRST_FRAME_TIMEOUT_MS });
1098
+ try { stream?.close(); } catch { /* already closing */ }
1099
+ try { session?.close(); } catch { /* already closing */ }
1100
+ this.http1Connection?.close();
1101
+ if (stream && session) {
1102
+ // close() waits for in-flight frames; a dead socket can ignore it — force-destroy shortly
1103
+ // after so a stalled TLS session cannot linger past the timeout.
1104
+ armTimeoutDestroyFallback(stream, session, this.input.timeoutDestroyGraceMs ?? CURSOR_TIMEOUT_DESTROY_GRACE_MS);
1105
+ }
1106
+ releaseBacklogLease();
1107
+ settler.settleFail(new Error("Cursor transport timed out before first response"));
1108
+ }, this.input.firstFrameTimeoutMs ?? CURSOR_FIRST_FRAME_TIMEOUT_MS);
1109
+
1110
+ // Raw Connect backlog with a parse cursor: appends copy only the incoming
1111
+ // chunk (amortized capacity growth), the consumed prefix is reclaimed
1112
+ // lazily, and the RAW used length (headers included) is what the 32 MiB
1113
+ // transport cap bounds — payload-only accounting let tiny-frame/header
1114
+ // floods slip through.
1115
+ let backlog = new Uint8Array();
1116
+ let backlogStart = 0;
1117
+ let backlogEnd = 0;
1118
+ const BACKLOG_COMPACT_MIN_SAVINGS = 64 * 1024;
1119
+ const appendBacklog = (chunk: Uint8Array): void => {
1120
+ const used = backlogEnd - backlogStart;
1121
+ let start = backlogStart;
1122
+ let end = backlogEnd;
1123
+ // Reclaim the consumed prefix when it is large or needed for capacity.
1124
+ if (start > 0 && (start >= BACKLOG_COMPACT_MIN_SAVINGS || end + chunk.byteLength > backlog.byteLength)) {
1125
+ backlog = backlog.slice(start, end);
1126
+ start = 0;
1127
+ end = used;
1128
+ }
1129
+ if (end + chunk.byteLength > backlog.byteLength) {
1130
+ const capacity = Math.max(8192, backlog.byteLength * 2, end + chunk.byteLength);
1131
+ const next = new Uint8Array(Math.min(CURSOR_TRANSPORT_MAX_BUFFERED_BYTES, capacity));
1132
+ next.set(backlog.subarray(start, end), 0);
1133
+ backlog = next;
1134
+ }
1135
+ backlog.set(chunk, end);
1136
+ backlogStart = start;
1137
+ backlogEnd = end + chunk.byteLength;
1138
+ };
1139
+ let frameWork: Promise<void> = Promise.resolve();
1140
+ // Idempotent terminal owner for the backlog lease: every settle/close path
1141
+ // must leave the raw charge at zero instead of relying on budget disposal.
1142
+ let backlogLeaseReleased = false;
1143
+ const releaseBacklogLease = () => {
1144
+ if (backlogLeaseReleased) return;
1145
+ backlogLeaseReleased = true;
1146
+ const leftover = backlogEnd - backlogStart;
1147
+ if (leftover > 0) this.releaseTransportBytes(leftover);
1148
+ backlog = new Uint8Array();
1149
+ backlogStart = 0;
1150
+ backlogEnd = 0;
1151
+ };
1152
+ const handleFrame = async (frame: ReturnType<typeof consumeConnectFrames>["frames"][number]) => {
1153
+ this.framesReceived++;
1154
+ if ((frame.flags & CONNECT_FLAG_END_STREAM) === CONNECT_FLAG_END_STREAM) {
1155
+ const endError = parseConnectEndStreamError(frame.payload);
1156
+ debugProviderDiagnostic("cursor", "connect-end-stream", endError ? {
1157
+ code: cursorConnectErrorCode(frame.payload),
1158
+ message: redactCursorForLog(endError.message),
1159
+ classified: classifyCursorError(endError.message),
1160
+ framesReceived: this.framesReceived,
1161
+ elapsedMs: Date.now() - this.turnStartedAt,
1162
+ } : { framesReceived: this.framesReceived, elapsedMs: Date.now() - this.turnStartedAt });
1163
+ if (endError) {
1164
+ failAndClear(endError);
1165
+ return;
1166
+ }
1167
+ // Connect's clean END_STREAM envelope is the protocol terminal. Cursor's RunSSE body can
1168
+ // remain open after this frame (or close through an AbortError), so waiting for HTTP EOF
1169
+ // strands an otherwise completed turn until the outer bridge stall watchdog fires.
1170
+ //
1171
+ // Earlier frames in this serialized frameWork chain have already run. Preserve their real
1172
+ // turnEnded terminal when present; otherwise finalize the clean protocol end once so open
1173
+ // tool calls still fail closed, a text-only turn receives its normal done event, and a
1174
+ // drained client-tool turn does not lose the pending terminal when protocol cleanup clears
1175
+ // its grace timer.
1176
+ const hasPendingClientToolFinalization = this.pendingFinalize !== undefined;
1177
+ if (
1178
+ !this.expectedClose
1179
+ && !state.terminated
1180
+ && !this.emittedTerminal
1181
+ && (
1182
+ state.openToolCalls.size > 0
1183
+ || this.sawAssistantText
1184
+ || hasPendingClientToolFinalization
1185
+ )
1186
+ ) {
1187
+ const terminal = hasPendingClientToolFinalization && state.openToolCalls.size === 0
1188
+ ? finalizeAfterDrain(state)
1189
+ : finalizeTurnEvents(state);
1190
+ for (const event of terminal) push(event);
1191
+ }
1192
+ this.markProtocolComplete();
1193
+ releaseBacklogLease();
1194
+ settler.settleFinish();
1195
+ return;
1196
+ }
1197
+ const decoded = fromBinary(AgentServerMessageSchema, frame.payload);
1198
+ // T04: every decoded frame refreshes the silence clock; only non-liveness frames
1199
+ // refresh the progress clock. First decoded frame arms the watchdog (the first-frame
1200
+ // timer owned everything before this point).
1201
+ const decodedUpdate = decoded.message.case === "interactionUpdate" ? decoded.message.value.message?.case : undefined;
1202
+ const livenessOnly = decodedUpdate === "heartbeat" || decoded.message.case === "conversationCheckpointUpdate";
1203
+ if (!this.streamHealthFail) {
1204
+ const now = Date.now();
1205
+ this.lastInboundFrameAt = now;
1206
+ this.lastMeaningfulFrameAt = now;
1207
+ this.streamHealthFail = failAndClear;
1208
+ }
1209
+ this.noteInboundFrame(livenessOnly);
1210
+ await this.handleServerMessage(decoded, state, push);
1211
+ };
1212
+ const drainPendingFrames = () => {
1213
+ const availableSlots = CURSOR_MAX_PENDING_FRAMES - this.pendingTransportFrames;
1214
+ const used = backlogEnd - backlogStart;
1215
+ if (availableSlots <= 0 || used === 0) {
1216
+ this.updateTransportFlowControl();
1217
+ return;
1218
+ }
1219
+ // Cursor decode, zero-copy: frame payloads are views into the backlog, so
1220
+ // the charge TRANSFERS — only consumed header bytes leave the counter
1221
+ // (payload bytes stay charged and are released when each frame's work
1222
+ // finishes). An exact 16 MiB payload therefore peaks at 16 MiB + 5, not
1223
+ // at double its size.
1224
+ const decoded = consumeConnectFrames(
1225
+ backlog.subarray(backlogStart, backlogEnd),
1226
+ CURSOR_MAX_EFFECTIVE_CONNECT_PAYLOAD_BYTES,
1227
+ availableSlots,
1228
+ );
1229
+ if (decoded.frames.length > 0) {
1230
+ backlogStart += decoded.consumedBytes;
1231
+ this.releaseTransportBytes(decoded.consumedBytes - decoded.frames.reduce((n, frame) => n + frame.payload.byteLength, 0));
1232
+ }
1233
+ for (const frame of decoded.frames) {
1234
+ this.pendingTransportFrames += 1;
1235
+ this.updateTransportFlowControl();
1236
+ frameWork = frameWork
1237
+ .then(() => handleFrame(frame))
1238
+ .catch(err => failAndClear(err instanceof Error ? err : new Error(String(err))))
1239
+ .finally(() => {
1240
+ this.releaseTransportBytes(frame.payload.byteLength);
1241
+ this.pendingTransportFrames = Math.max(0, this.pendingTransportFrames - 1);
1242
+ this.updateTransportFlowControl();
1243
+ drainPendingFrames();
1244
+ });
1245
+ }
1246
+ };
1247
+ const onData = (chunk: string | Uint8Array) => {
1248
+ this.clearFirstFrameTimer();
1249
+ // Once the turn has settled, late network bytes must never be charged —
1250
+ // the backlog lease is already released and nobody would own these.
1251
+ if (settler.settled()) return;
1252
+ if (!this.firstFrameLogged) {
1253
+ this.firstFrameLogged = true;
1254
+ this.firstFrameAt = Date.now();
1255
+ debugProviderDiagnostic("cursor", "first-frame", { latencyMs: this.firstFrameAt - this.turnStartedAt });
1256
+ }
1257
+ const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
1258
+ let charged = false;
1259
+ let appended = false;
1260
+ try {
1261
+ // RAW chunk bytes (headers included) join the backlog charge; consumed
1262
+ // bytes leave it at drain. No whole-backlog replacement copy anymore.
1263
+ this.reserveTransportBytes(bytes.byteLength);
1264
+ charged = true;
1265
+ appendBacklog(bytes);
1266
+ appended = true;
1267
+ drainPendingFrames();
1268
+ } catch (err) {
1269
+ // Release ONLY when the reservation succeeded but the append never
1270
+ // happened. A failed reservation charged nothing — releasing here
1271
+ // would debit unrelated existing ownership; an appended chunk is owned
1272
+ // by the terminal backlog cleanup.
1273
+ if (charged && !appended) this.releaseTransportBytes(bytes.byteLength);
1274
+ failAndClear(err instanceof Error ? err : new Error(String(err)));
1275
+ }
1276
+ };
1277
+ const onTrailers = (trailers: http2.IncomingHttpHeaders) => {
1278
+ const status = trailers["grpc-status"];
1279
+ if (status !== undefined) debugProviderDiagnostic("cursor", "trailers", { grpcStatus: String(status) });
1280
+ if (status && status !== "0") failAndClear(new Error(`Cursor gRPC error ${status}`));
1281
+ };
1282
+ const onStreamError = (err: unknown) => {
1283
+ const realErr = err instanceof Error ? err : new Error(String(err));
1284
+ if (this.expectedClose) {
1285
+ failAndClear(realErr);
1286
+ return;
1287
+ }
1288
+ const code = (realErr as { code?: unknown }).code;
1289
+ const errno = (realErr as { errno?: unknown }).errno;
1290
+ debugProviderDiagnostic("cursor", "stream-error", {
1291
+ code: typeof code === "string" || typeof code === "number" ? String(code) : undefined,
1292
+ errno: typeof errno === "string" || typeof errno === "number" ? String(errno) : undefined,
1293
+ name: realErr.name,
1294
+ message: redactCursorForLog(realErr.message),
1295
+ committed: this.committed,
1296
+ framesReceived: this.framesReceived,
1297
+ elapsedMs: Date.now() - this.turnStartedAt,
1298
+ });
1299
+ failAndClear(realErr);
1300
+ };
1301
+ const onStreamEnd = () => {
1302
+ this.clearFirstFrameTimer();
1303
+ debugProviderDiagnostic("cursor", "stream-end", {
1304
+ committed: this.committed,
1305
+ framesReceived: this.framesReceived,
1306
+ expectedClose: this.expectedClose,
1307
+ elapsedMs: Date.now() - this.turnStartedAt,
1308
+ });
1309
+ // Settle only after queued frame work drains to quiescence (the chain can
1310
+ // extend itself while draining), then classify the terminal state:
1311
+ // a trailing incomplete frame is a typed failure, and a zero-frame,
1312
+ // zero-byte end without an expected close stays the existing unexpected
1313
+ // EOF — both beat the old silent success.
1314
+ void (async () => {
1315
+ let previous: Promise<void>;
1316
+ do {
1317
+ previous = frameWork;
1318
+ await previous;
1319
+ } while (previous !== frameWork);
1320
+ })().then(() => {
1321
+ if (settler.settled()) return;
1322
+ const leftover = backlogEnd - backlogStart;
1323
+ if (leftover > 0 && !this.expectedClose) {
1324
+ releaseBacklogLease();
1325
+ settler.settleFail(new ConnectFrameError(
1326
+ "frame_incomplete",
1327
+ `Cursor Connect stream ended with ${leftover} unconsumed bytes (incomplete frame)`,
1328
+ ));
1329
+ return;
1330
+ }
1331
+ if (this.framesReceived === 0 && !this.expectedClose) {
1332
+ releaseBacklogLease();
1333
+ settler.settleFail(new Error("Cursor stream ended before any response frame (unexpected EOF)"));
1334
+ return;
1335
+ }
1336
+ // `emittedTerminal` joins dev's two conditions so EOF finalization cannot append a
1337
+ // second terminal after a mapper error already failed the turn (integration 010).
1338
+ if (state.terminated || this.expectedClose || this.emittedTerminal) {
1339
+ releaseBacklogLease();
1340
+ settler.settleFinish();
1341
+ return;
1342
+ }
1343
+ // Open tools fail-closed as a truncation *event* (finalizeTurnEvents), not a thrown
1344
+ // transport error. settleFail here would hide that typed message as adapter_eof.
1345
+ if (state.openToolCalls.size > 0) {
1346
+ for (const event of finalizeTurnEvents(state)) push(event);
1347
+ releaseBacklogLease();
1348
+ settler.settleFinish();
1349
+ return;
1350
+ }
1351
+ if (this.framesReceived > 0 && this.sawAssistantText) {
1352
+ for (const event of finalizeTurnEvents(state)) push(event);
1353
+ releaseBacklogLease();
1354
+ settler.settleFinish();
1355
+ return;
1356
+ }
1357
+ releaseBacklogLease();
1358
+ settler.settleFinish();
1359
+ }).catch((err) => {
1360
+ failAndClear(err instanceof Error ? err : new Error(String(err)));
1361
+ });
1362
+ };
1363
+
1364
+ if (useHttp1) {
1365
+ const providerFetch = this.input.fetch
1366
+ ?? (this.input.provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch;
1367
+ this.http1Connection = new CursorHttp1BidiConnection({
1368
+ baseUrl,
1369
+ token: this.token,
1370
+ clientVersion: CURSOR_CLIENT_VERSION,
1371
+ sessionId: this.sessionId,
1372
+ requestId,
1373
+ translatorBudget: this.translatorBudget,
1374
+ callbacks: {
1375
+ onCommitted: () => {
1376
+ this.committed = true;
1377
+ debugProviderDiagnostic("cursor", "connected", {
1378
+ transport: "http1.1",
1379
+ connectMs: Date.now() - this.turnStartedAt,
1380
+ });
1381
+ },
1382
+ onData,
1383
+ onEnd: onStreamEnd,
1384
+ onError: onStreamError,
1385
+ },
1386
+ ...(providerFetch ? { fetch: providerFetch } : {}),
1387
+ });
1388
+ this.http1Connection.start();
1389
+ } else {
1390
+ session!.on("error", onSessionError);
1391
+ stream!.on("data", onData);
1392
+ stream!.on("trailers", onTrailers);
1393
+ stream!.on("error", onStreamError);
1394
+ stream!.on("end", onStreamEnd);
1395
+ }
1396
+
1397
+ signal?.addEventListener("abort", () => {
1398
+ this.close();
1399
+ failAndClear(new Error("Cursor request was aborted"));
1400
+ }, { once: true });
1401
+ // Close the race between the preflight above and listener installation. No request payload is
1402
+ // written until after this check.
1403
+ if (signal?.aborted) {
1404
+ this.close();
1405
+ failAndClear(signal.reason instanceof Error ? signal.reason : new Error("Cursor request was aborted"));
1406
+ return;
1407
+ }
1408
+
1409
+ this.writeConnectFrame(encodeConnectFrame(encodedRequest));
1410
+ this.heartbeat = setInterval(() => {
1411
+ this.writeConnectFrame(encodeClientMessage({
1412
+ message: { case: "clientHeartbeat", value: create(ClientHeartbeatSchema, {}) },
1413
+ }));
1414
+ }, HEARTBEAT_MS);
1415
+ }
1416
+
1417
+ capturedConversationCheckpoint(): Uint8Array | undefined {
1418
+ return this.capturedCheckpointBytes;
1419
+ }
1420
+
1421
+ private async handleServerMessage(
1422
+ message: AgentServerMessage,
1423
+ state: ReturnType<typeof createCursorProtobufEventState>,
1424
+ push: (message: CursorServerMessage) => void,
1425
+ ): Promise<void> {
1426
+ if (!this.stream && !this.http1Connection) return;
1427
+ debugProviderDiagnostic("cursor", "frame", describeCursorServerFrame(message));
1428
+ if (message.message.case === "conversationCheckpointUpdate") {
1429
+ try {
1430
+ this.capturedCheckpointBytes = toBinary(ConversationStateStructureSchema, message.message.value);
1431
+ } catch {
1432
+ this.capturedCheckpointBytes = undefined;
1433
+ }
1434
+ }
1435
+ if (message.message.case === "kvServerMessage") {
1436
+ this.writeConnectFrame(encodeConnectFrame(handleCursorNativeKv(message.message.value, this.blobRequestScope)));
1437
+ return;
1438
+ }
1439
+ if (message.message.case === "execServerMessage") {
1440
+ const execMsg = message.message.value;
1441
+ if (execMsg.message.case === "mcpArgs") {
1442
+ const plan = planMcpArgsHandling(execMsg, state);
1443
+ if (plan.handledByResponsesBridge) {
1444
+ this.noteClientToolActivity();
1445
+ for (const event of plan.events) push(event);
1446
+ if (plan.cancelCursorRun) this.cancelCursorRun();
1447
+ else if (plan.finalizeWhenDrained) this.scheduleClientToolFinalize(state, push);
1448
+ return;
1449
+ }
1450
+ }
1451
+ // Native exec/MCP is handled inside this transport and can mutate files/process state
1452
+ // without emitting a Responses tool event. Mark the turn replay-unsafe before executing so
1453
+ // an eventual invalid_argument cannot cause the adapter's fresh-conversation fallback to
1454
+ // run the same local action twice.
1455
+ push({ type: "local_side_effect" });
1456
+ const replies = await handleCursorNativeExec(message.message.value, this.execContext);
1457
+ for (const reply of replies) this.writeConnectFrame(encodeConnectFrame(reply));
1458
+ return;
1459
+ }
1460
+ if (message.message.case === "interactionQuery") {
1461
+ // The server-side agent BLOCKS until this query is answered with the matching id; leaving it
1462
+ // unanswered is the proven stall → watchdog → upstream-502 mechanism. Reply immediately with
1463
+ // the non-interactive default and emit liveness so the bridge watchdog sees progress.
1464
+ const query = message.message.value;
1465
+ const plan = planInteractionQueryReply(query);
1466
+ debugProviderDiagnostic("cursor", "interaction-query", { id: query.id, queryCase: query.query.case ?? "unknown", reply: plan.replyCase });
1467
+ this.writeConnectFrame(encodeClientMessage({ message: { case: "interactionResponse", value: plan.response } }));
1468
+ if (!state.terminated) {
1469
+ if (plan.planText) {
1470
+ this.sawAssistantText = true;
1471
+ push({ type: "text", text: plan.planText });
1472
+ }
1473
+ push({ type: "heartbeat" });
1474
+ }
1475
+ return;
1476
+ }
1477
+ // A completion may carry only callId. Capture its ownership before mapping removes the open
1478
+ // call, because the embedded-tool classifier cannot identify that valid compact frame.
1479
+ const update = message.message.case === "interactionUpdate" ? message.message.value.message : undefined;
1480
+ if (update?.case === "turnEnded") {
1481
+ // T03: the application turn is complete. Close our side of HTTP/2 after a short
1482
+ // grace so a held-open server response cannot pin the turn to the bridge's idle
1483
+ // timeout (senpi #1062). finalizeTurnEvents already emitted done via the mapper.
1484
+ this.closeAfterTurnEnded();
1485
+ }
1486
+ const completesOpenClientTool = update?.case === "toolCallCompleted"
1487
+ && state.openToolCalls.has(update.value.callId);
1488
+ const awaitedNativeArgsBeforeMapping = update?.case === "toolCallCompleted"
1489
+ && state.openToolCalls.get(update.value.callId)?.awaitingNativeArgs === true;
1490
+ const mapped = mapCursorProtobufServerMessage(message, state);
1491
+ if (mapped.some(event => event.type === "text")) this.sawAssistantText = true;
1492
+ const beganAwaitingNativeClientToolArgs = update?.case === "toolCallCompleted"
1493
+ && !awaitedNativeArgsBeforeMapping
1494
+ && state.openToolCalls.get(update.value.callId)?.awaitingNativeArgs === true;
1495
+ if (mapped.length > 0) {
1496
+ // A client tool call announced/committed via interactionUpdate (toolCallStarted/partialToolCall/
1497
+ // toolCallCompleted) changes the call set, so revoke any finalize armed by an earlier drain.
1498
+ // A completion can also commit and drain a late call without a following mcpArgs frame; in
1499
+ // that case re-arm finalization here so the Responses bridge does not wait forever.
1500
+ const clientToolFrame = completesOpenClientTool || isClientToolFrame(message);
1501
+ if (clientToolFrame) this.noteClientToolActivity();
1502
+ for (const event of mapped) push(event);
1503
+ if (
1504
+ clientToolFrame
1505
+ && state.openToolCalls.size === 0
1506
+ && mapped.some(event => event.type === "tool_call_end")
1507
+ ) this.scheduleClientToolFinalize(state, push);
1508
+ return;
1509
+ }
1510
+ // The frame produced no outward Responses event (e.g. toolCallStarted / partialToolCall args
1511
+ // buffering, a completion waiting for native args, toolCallDelta, tokenDelta, or a checkpoint
1512
+ // update). Tool-call protocol events are deferred to completion for atomic, parallel-safe
1513
+ // emission, so a turn that silently assembles several tool calls can otherwise exceed the
1514
+ // bridge's stall watchdog (upstream_stall_timeout).
1515
+ // Emit a liveness heartbeat for these progress frames so the watchdog sees the upstream is alive.
1516
+ // Never after a terminal (done/truncation): a stray post-terminal frame must stay fully inert.
1517
+ if (!state.terminated && (isCursorProgressFrame(message) || beganAwaitingNativeClientToolArgs)) {
1518
+ if (isClientToolFrame(message) || beganAwaitingNativeClientToolArgs) this.noteClientToolActivity();
1519
+ push({ type: "heartbeat" });
1520
+ }
1521
+ }
1522
+ }
1523
+
1524
+ /**
1525
+ * Build the best-effort partial usage for a turn that failed before a clean `done` (upstream 502,
1526
+ * stream error, abort). Mirrors the clean-finalize math in `finalizeTurnEvents`: the last absolute
1527
+ * checkpoint (`contextTokens`) is the cumulative context, the streamed delta stays in outputTokens.
1528
+ * Returns undefined when the stream died before ANY token signal (nothing meaningful to report).
1529
+ * Exported for unit testing.
1530
+ */
1531
+ export function partialUsageFromEventState(state: ReturnType<typeof createCursorProtobufEventState>): OcxUsage | undefined {
1532
+ const out = state.usage.outputTokens;
1533
+ const hasCurrentCheckpoint = Number.isFinite(state.contextTokens) && (state.contextTokens ?? 0) > 0;
1534
+ const hasCurrentOutput = Number.isFinite(out) && out > 0;
1535
+ // A carry-forward value belongs to an earlier successful turn. It can complete current-turn
1536
+ // usage math after this turn emits output, but cannot by itself prove that a first-frame failure
1537
+ // consumed anything.
1538
+ if (!hasCurrentCheckpoint && !hasCurrentOutput) return undefined;
1539
+ // Same resolution order as a clean turn, so a failed turn does not silently drop
1540
+ // back to inputTokens=0 when only the request-local estimate is available (#373).
1541
+ return { ...resolvedTurnUsage(state), estimated: true };
1542
+ }
1543
+
1544
+ /**
1545
+ * Attach partial usage to a transport failure so the adapter's error path can surface real token
1546
+ * consumption for 502/stall rows instead of `usageStatus: unreported` with 0 tokens.
1547
+ */
1548
+ function attachPartialUsage(failure: Error, state: ReturnType<typeof createCursorProtobufEventState>): Error {
1549
+ const usage = partialUsageFromEventState(state);
1550
+ if (usage) (failure as Error & { partialUsage?: OcxUsage }).partialUsage = usage;
1551
+ return failure;
1552
+ }
1553
+
1554
+ /**
1555
+ * Compact frame descriptor for provider debug (`ocx debug provider on`): outer case plus the inner
1556
+ * interactionUpdate/exec case and tool-call union case when present. No payload content is logged.
1557
+ */
1558
+ function describeCursorServerFrame(message: AgentServerMessage): Record<string, unknown> {
1559
+ const out: Record<string, unknown> = { case: message.message.case ?? "unknown" };
1560
+ if (message.message.case === "interactionUpdate") {
1561
+ const update = message.message.value.message;
1562
+ out.update = update.case ?? "unknown";
1563
+ if (update.case === "toolCallStarted" || update.case === "partialToolCall" || update.case === "toolCallCompleted") {
1564
+ out.toolCase = update.value.toolCall?.tool.case ?? "none";
1565
+ out.callId = update.value.callId;
1566
+ }
1567
+ } else if (message.message.case === "execServerMessage") {
1568
+ out.exec = message.message.value.message.case ?? "unknown";
1569
+ } else if (message.message.case === "interactionQuery") {
1570
+ out.query = message.message.value.query.case ?? "unknown";
1571
+ out.id = message.message.value.id;
1572
+ } else if (message.message.case === "kvServerMessage") {
1573
+ out.kv = message.message.value.message.case ?? "unknown";
1574
+ } else if (message.message.case === "conversationCheckpointUpdate") {
1575
+ out.usedTokens = message.message.value.tokenDetails?.usedTokens ?? 0;
1576
+ }
1577
+ return out;
1578
+ }
1579
+
1580
+ /**
1581
+ * True when a server frame represents real upstream progress that produced no outward Responses
1582
+ * event (so the bridge's stall watchdog would otherwise see silence). Covers tool-call assembly,
1583
+ * token/checkpoint accounting — the frames `mapCursorProtobufServerMessage` intentionally swallows.
1584
+ */
1585
+ function isCursorProgressFrame(message: AgentServerMessage): boolean {
1586
+ if (message.message.case === "conversationCheckpointUpdate") return true;
1587
+ if (message.message.case !== "interactionUpdate") return false;
1588
+ switch (message.message.value.message.case) {
1589
+ case "toolCallStarted":
1590
+ case "partialToolCall":
1591
+ case "toolCallDelta":
1592
+ case "tokenDelta":
1593
+ return true;
1594
+ default:
1595
+ return false;
1596
+ }
1597
+ }
1598
+
1599
+ /**
1600
+ * A tool-call lifecycle frame that can change the CLIENT tool call set (announce a new sibling or
1601
+ * commit one). Used to revoke a pending finalize so a late-announced parallel call is never dropped.
1602
+ * Only frames whose inner ToolCall is an ocx-bridged Responses tool (`mcpToolCall` with our provider)
1603
+ * count: Cursor-native tool frames (readToolCall/editToolCall/...) are display-plane and must not
1604
+ * revoke a pending client-tool finalize. Exported for unit testing.
1605
+ */
1606
+ export function isClientToolFrame(message: AgentServerMessage): boolean {
1607
+ if (message.message.case !== "interactionUpdate") return false;
1608
+ const update = message.message.value.message;
1609
+ switch (update.case) {
1610
+ case "toolCallStarted":
1611
+ case "partialToolCall":
1612
+ case "toolCallCompleted":
1613
+ return mcpArgsFromToolCall(update.value.toolCall) !== undefined;
1614
+ default:
1615
+ return false;
1616
+ }
1617
+ }
1618
+
1619
+ /** Host-only label for Cursor transport diagnostics — never leaks path/query/credentials. */
1620
+ function cursorHostLabel(baseUrl: string): string {
1621
+ try {
1622
+ return new URL(baseUrl).host;
1623
+ } catch {
1624
+ return "cursor";
1625
+ }
1626
+ }
1627
+
1628
+ /** Redact a Cursor error message for diagnostic output. Cursor error strings can carry raw
1629
+ * credential key=value pairs beyond what redactSecretString covers; safeCursorErrorMessage
1630
+ * already applies the full sanitizer plus the classified prefix, so reuse it verbatim. */
1631
+ function redactCursorForLog(message: string): string {
1632
+ return safeCursorErrorMessage(message).slice(0, 300);
1633
+ }
1634
+
1635
+ /** Extract the Connect end-stream `error.code` from the raw trailer frame payload without
1636
+ * surfacing the (potentially secret-bearing) message — used for `[ocx:cursor:connect-end-stream]`
1637
+ * diagnostics. Returns undefined when the payload is not the expected Connect error shape. */
1638
+ function cursorConnectErrorCode(payload: Uint8Array): string | undefined {
1639
+ try {
1640
+ const parsed = JSON.parse(new TextDecoder().decode(payload)) as { error?: { code?: string } };
1641
+ return parsed?.error?.code;
1642
+ } catch {
1643
+ return undefined;
1644
+ }
1645
+ }
1646
+
1647
+ export function createLiveCursorTransport(input: CursorTransportFactoryInput): CursorTransport {
1648
+ return new LiveCursorTransport(input);
1649
+ }
1650
+
1651
+ export function capturedCursorCheckpointBytes(transport: CursorTransport): Uint8Array | undefined {
1652
+ return transport.capturedConversationCheckpoint?.();
1653
+ }