@iislee/opencodex 2.11.0 → 2.35.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 (733) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +117 -22
  3. package/bin/ocx.mjs +245 -88
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  6. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +21 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +475 -62
  44. package/src/adapters/base.ts +82 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +637 -0
  48. package/src/adapters/cursor/call-id.ts +44 -0
  49. package/src/adapters/cursor/catalog.ts +541 -0
  50. package/src/adapters/cursor/checkpoint-store.ts +308 -0
  51. package/src/adapters/cursor/cursor-errors.ts +144 -6
  52. package/src/adapters/cursor/discovery.ts +122 -14
  53. package/src/adapters/cursor/effort-map.ts +106 -4
  54. package/src/adapters/cursor/envelope-echo.ts +290 -0
  55. package/src/adapters/cursor/framing.ts +39 -0
  56. package/src/adapters/cursor/h2-pool.ts +123 -0
  57. package/src/adapters/cursor/http1-bidi.ts +361 -0
  58. package/src/adapters/cursor/images.ts +704 -0
  59. package/src/adapters/cursor/live-models.ts +180 -59
  60. package/src/adapters/cursor/live-transport.ts +623 -170
  61. package/src/adapters/cursor/message-mapper.ts +4 -1
  62. package/src/adapters/cursor/native-exec-common.ts +23 -2
  63. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  64. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  65. package/src/adapters/cursor/native-exec-network.ts +1 -1
  66. package/src/adapters/cursor/native-exec-shell.ts +5 -3
  67. package/src/adapters/cursor/native-exec.ts +121 -14
  68. package/src/adapters/cursor/protobuf-events.ts +829 -11
  69. package/src/adapters/cursor/protobuf-request.ts +447 -75
  70. package/src/adapters/cursor/request-builder.ts +271 -35
  71. package/src/adapters/cursor/tool-definitions.ts +252 -12
  72. package/src/adapters/cursor/tool-result-normalize.ts +115 -0
  73. package/src/adapters/cursor/transport.ts +22 -0
  74. package/src/adapters/cursor/types.ts +48 -1
  75. package/src/adapters/cursor.ts +336 -39
  76. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  77. package/src/adapters/google-antigravity-replay.ts +713 -41
  78. package/src/adapters/google-antigravity-wire.ts +48 -10
  79. package/src/adapters/google-errors.ts +44 -12
  80. package/src/adapters/google-http.ts +38 -10
  81. package/src/adapters/google-tool-schema.ts +80 -15
  82. package/src/adapters/google-truncation.ts +11 -0
  83. package/src/adapters/google.ts +742 -86
  84. package/src/adapters/identity.ts +39 -6
  85. package/src/adapters/image.ts +1 -1
  86. package/src/adapters/kiro-constants.ts +27 -0
  87. package/src/adapters/kiro-errors.ts +11 -0
  88. package/src/adapters/kiro-events.ts +19 -1
  89. package/src/adapters/kiro-thinking.ts +18 -2
  90. package/src/adapters/kiro-tools.ts +65 -17
  91. package/src/adapters/kiro.ts +270 -73
  92. package/src/adapters/mimo-free.ts +17 -0
  93. package/src/adapters/openai-chat-url.ts +11 -0
  94. package/src/adapters/openai-chat.ts +1377 -344
  95. package/src/adapters/openai-responses-url.ts +16 -0
  96. package/src/adapters/openai-responses.ts +1164 -64
  97. package/src/adapters/registry.ts +175 -0
  98. package/src/adapters/responses-tool-schema.ts +67 -0
  99. package/src/adapters/run-turn-queue.ts +36 -1
  100. package/src/adapters/tool-call-id.ts +119 -0
  101. package/src/adapters/tool-catalog-nudge.ts +105 -21
  102. package/src/adapters/xai-tool-schema.ts +436 -0
  103. package/src/adapters/xai-web-search.ts +186 -0
  104. package/src/bridge.ts +512 -92
  105. package/src/chat/inbound.ts +57 -20
  106. package/src/chat/outbound.ts +97 -33
  107. package/src/claude/agents-inject.ts +40 -10
  108. package/src/claude/context-windows.ts +37 -14
  109. package/src/claude/desktop-3p.ts +243 -9
  110. package/src/claude/gateway-cache.ts +41 -4
  111. package/src/claude/inbound.ts +72 -3
  112. package/src/claude/model-info.ts +38 -15
  113. package/src/claude/outbound.ts +88 -21
  114. package/src/cli/access.ts +46 -3
  115. package/src/cli/account-api.ts +150 -19
  116. package/src/cli/account-auth.ts +33 -6
  117. package/src/cli/account-catalog-refresh.ts +14 -0
  118. package/src/cli/account-extended.ts +648 -28
  119. package/src/cli/account-main.ts +317 -0
  120. package/src/cli/account.ts +97 -19
  121. package/src/cli/agent.ts +53 -2
  122. package/src/cli/alias.ts +66 -0
  123. package/src/cli/capabilities-command.ts +94 -0
  124. package/src/cli/capabilities.ts +496 -0
  125. package/src/cli/claude-agent-startup-sync.ts +73 -0
  126. package/src/cli/claude-desktop.ts +60 -15
  127. package/src/cli/claude.ts +129 -35
  128. package/src/cli/codex-log-guard-doctor.ts +103 -0
  129. package/src/cli/codex-shim-autorestore.ts +2 -0
  130. package/src/cli/codex-shim-readiness.ts +76 -0
  131. package/src/cli/combo.ts +8 -0
  132. package/src/cli/config-command.ts +74 -10
  133. package/src/cli/dispatch.ts +821 -0
  134. package/src/cli/doctor.ts +440 -56
  135. package/src/cli/ensure-desired-integrations.ts +152 -0
  136. package/src/cli/export-command.ts +46 -20
  137. package/src/cli/help.ts +34 -274
  138. package/src/cli/index.ts +417 -528
  139. package/src/cli/init.ts +5 -17
  140. package/src/cli/inspect.ts +230 -0
  141. package/src/cli/integrations.ts +120 -2
  142. package/src/cli/lab.ts +607 -0
  143. package/src/cli/launcher-context.ts +77 -0
  144. package/src/cli/minimax.ts +497 -0
  145. package/src/cli/models-runtime.ts +130 -2
  146. package/src/cli/models.ts +118 -16
  147. package/src/cli/observe.ts +123 -11
  148. package/src/cli/opencode.ts +4 -2
  149. package/src/cli/provider-runtime.ts +34 -3
  150. package/src/cli/provider.ts +25 -3
  151. package/src/cli/ready.ts +301 -0
  152. package/src/cli/registry.ts +476 -0
  153. package/src/cli/root.ts +86 -0
  154. package/src/cli/route-policy.ts +92 -0
  155. package/src/cli/runtime-api.ts +57 -10
  156. package/src/cli/star-prompt.ts +71 -15
  157. package/src/cli/status.ts +51 -4
  158. package/src/cli/storage.ts +234 -0
  159. package/src/cli/system-command.ts +16 -0
  160. package/src/cli/system-restart-client.ts +146 -0
  161. package/src/cli/tray-proxy.ts +153 -6
  162. package/src/cli/usage-report.ts +184 -0
  163. package/src/cli/v2.ts +137 -18
  164. package/src/cli/version-skew.ts +46 -0
  165. package/src/cli.ts +1 -1
  166. package/src/clients/config-export.ts +1442 -23
  167. package/src/clients/effective-status.ts +30 -2
  168. package/src/clients/probes/cline.ts +81 -0
  169. package/src/codex/account-label.ts +35 -1
  170. package/src/codex/account-lifecycle.ts +130 -13
  171. package/src/codex/account-namespaces.ts +49 -3
  172. package/src/codex/account-pause.ts +2 -1
  173. package/src/codex/account-priority.ts +84 -0
  174. package/src/codex/account-store.ts +29 -2
  175. package/src/codex/account-usability.ts +25 -2
  176. package/src/codex/admission.ts +256 -0
  177. package/src/codex/affinity-debug.ts +162 -0
  178. package/src/codex/app-server-processes.ts +570 -107
  179. package/src/codex/app-server-restart-service.ts +232 -0
  180. package/src/codex/auth-api.ts +893 -246
  181. package/src/codex/auth-collision.ts +5 -3
  182. package/src/codex/auth-context.ts +348 -32
  183. package/src/codex/autostart-health.ts +24 -1
  184. package/src/codex/catalog/account-models.ts +67 -0
  185. package/src/codex/catalog/aggregation.ts +86 -10
  186. package/src/codex/catalog/bundled.ts +331 -33
  187. package/src/codex/catalog/effort.ts +138 -32
  188. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  189. package/src/codex/catalog/kinds.ts +2 -0
  190. package/src/codex/catalog/metadata.ts +555 -45
  191. package/src/codex/catalog/native-models.ts +75 -0
  192. package/src/codex/catalog/parsing.ts +269 -30
  193. package/src/codex/catalog/provider-fetch.ts +1626 -144
  194. package/src/codex/catalog/sync.ts +1576 -187
  195. package/src/codex/catalog-admission.ts +199 -0
  196. package/src/codex/catalog-refresh-status.ts +105 -0
  197. package/src/codex/catalog-write-serialization.ts +242 -0
  198. package/src/codex/catalog.ts +6 -3
  199. package/src/codex/codex-write-lock.ts +386 -0
  200. package/src/codex/convergence-types.ts +614 -0
  201. package/src/codex/convergence.ts +676 -0
  202. package/src/codex/coordinator-doctor.ts +332 -0
  203. package/src/codex/custom-model-catalog-migration.ts +176 -0
  204. package/src/codex/desired-state.ts +230 -0
  205. package/src/codex/desktop-app-restart.ts +355 -0
  206. package/src/codex/features.ts +636 -39
  207. package/src/codex/generation.ts +202 -0
  208. package/src/codex/history-job.ts +436 -0
  209. package/src/codex/history-lock.ts +242 -0
  210. package/src/codex/history-manifest.ts +112 -0
  211. package/src/codex/history-migration-guardian.ts +30 -24
  212. package/src/codex/history-provider.ts +1016 -235
  213. package/src/codex/history-transition.ts +105 -0
  214. package/src/codex/history-worker.ts +223 -0
  215. package/src/codex/inject-coordination.ts +373 -0
  216. package/src/codex/inject.ts +1114 -152
  217. package/src/codex/injected-marker.ts +37 -3
  218. package/src/codex/integration-record.ts +266 -0
  219. package/src/codex/internal/catalog-writer.ts +203 -0
  220. package/src/codex/internal/history-writer.ts +81 -0
  221. package/src/codex/journal.ts +66 -4
  222. package/src/codex/log-guard/inspect.ts +524 -0
  223. package/src/codex/log-guard/lock.ts +150 -0
  224. package/src/codex/log-guard/maintenance.ts +403 -0
  225. package/src/codex/log-guard/path-safety.ts +88 -0
  226. package/src/codex/log-guard/policy.ts +44 -0
  227. package/src/codex/log-guard/processes.ts +205 -0
  228. package/src/codex/log-guard/protection.ts +489 -0
  229. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  230. package/src/codex/main-account-cache.ts +24 -0
  231. package/src/codex/main-account.ts +29 -1
  232. package/src/codex/management-convergence.ts +167 -0
  233. package/src/codex/model-cache.ts +56 -10
  234. package/src/codex/model-entitlements.ts +360 -0
  235. package/src/codex/native-main-admission.ts +47 -0
  236. package/src/codex/native-main-auth-temp.ts +187 -0
  237. package/src/codex/native-main-claim.ts +178 -0
  238. package/src/codex/native-main-lock-file.ts +162 -0
  239. package/src/codex/native-main-owner.ts +329 -0
  240. package/src/codex/native-profile-api.ts +247 -0
  241. package/src/codex/native-profile-manager.ts +1531 -0
  242. package/src/codex/native-profile-processes.ts +121 -0
  243. package/src/codex/native-profile-recovery.ts +99 -0
  244. package/src/codex/native-profile-stage-store.ts +387 -0
  245. package/src/codex/native-profile-startup.ts +622 -0
  246. package/src/codex/native-profile-store.ts +855 -0
  247. package/src/codex/native-profile-types.ts +120 -0
  248. package/src/codex/native-residue.ts +675 -0
  249. package/src/codex/paths.ts +80 -1
  250. package/src/codex/plan-from-token.ts +140 -0
  251. package/src/codex/plan.ts +40 -0
  252. package/src/codex/plugins-doctor.ts +1 -1
  253. package/src/codex/pool-rotation.ts +74 -4
  254. package/src/codex/project-config-warnings.ts +20 -6
  255. package/src/codex/prompt-journal.ts +352 -0
  256. package/src/codex/prompt-layers.ts +1313 -0
  257. package/src/codex/prompt-lock.ts +143 -0
  258. package/src/codex/prompt-text-probe.ts +238 -0
  259. package/src/codex/quota-rejection.ts +298 -0
  260. package/src/codex/quota.ts +264 -21
  261. package/src/codex/refresh.ts +11 -2
  262. package/src/codex/reset-credit-recovery.ts +1044 -0
  263. package/src/codex/routing.ts +514 -94
  264. package/src/codex/runtime.ts +159 -38
  265. package/src/codex/shim.ts +1065 -31
  266. package/src/codex/subagent-model-fallback.ts +437 -43
  267. package/src/codex/sync.ts +191 -2
  268. package/src/codex/transition-state.ts +720 -0
  269. package/src/codex/upstream-host-health.ts +368 -0
  270. package/src/codex/user-identity.ts +557 -0
  271. package/src/codex/warmup.ts +187 -81
  272. package/src/codex/write-coordination.ts +114 -0
  273. package/src/combos/failover.ts +47 -0
  274. package/src/combos/index.ts +4 -0
  275. package/src/combos/request.ts +32 -0
  276. package/src/combos/types.ts +91 -9
  277. package/src/compatibility/index.ts +26 -0
  278. package/src/compatibility/manifest.ts +253 -0
  279. package/src/compatibility/openai-responses.ts +121 -0
  280. package/src/config/atomic-write.ts +219 -0
  281. package/src/config/paths.ts +40 -0
  282. package/src/config/process-state.ts +309 -0
  283. package/src/config/provider-name.ts +24 -0
  284. package/src/config/provider-validation.ts +177 -0
  285. package/src/config/rebase-provenance.ts +68 -0
  286. package/src/config.ts +1713 -709
  287. package/src/generated/compatibility-version.json +3324 -0
  288. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  289. package/src/grok/inject.ts +16 -5
  290. package/src/grok/inspect.ts +45 -0
  291. package/src/grok/sync.ts +2 -2
  292. package/src/images/loop.ts +157 -33
  293. package/src/images/plan.ts +24 -13
  294. package/src/integrations/config-io.ts +269 -0
  295. package/src/integrations/journal.ts +315 -0
  296. package/src/integrations/merge.ts +135 -0
  297. package/src/integrations/mutation-flight.ts +71 -0
  298. package/src/integrations/native/ownership-preflight.ts +202 -0
  299. package/src/integrations/omp-yaml-source.ts +358 -0
  300. package/src/integrations/owned-refresh.ts +74 -0
  301. package/src/integrations/ownership-policy.ts +160 -0
  302. package/src/integrations/ownership.ts +155 -0
  303. package/src/integrations/registry.ts +166 -0
  304. package/src/integrations/serialize.ts +314 -0
  305. package/src/integrations/state.ts +433 -0
  306. package/src/integrations/store.ts +103 -0
  307. package/src/integrations/writer-lock.ts +98 -0
  308. package/src/integrations/writer.ts +715 -0
  309. package/src/lab/artifacts/sanitize.ts +586 -0
  310. package/src/lab/artifacts/secure-fs.ts +475 -0
  311. package/src/lab/artifacts/store.ts +310 -0
  312. package/src/lab/automation/budgets.ts +78 -0
  313. package/src/lab/automation/config-persistence.ts +256 -0
  314. package/src/lab/automation/constants.ts +39 -0
  315. package/src/lab/automation/cooldown.ts +103 -0
  316. package/src/lab/automation/dispatch.ts +211 -0
  317. package/src/lab/automation/index.ts +13 -0
  318. package/src/lab/automation/orchestrator.ts +499 -0
  319. package/src/lab/automation/persistence.ts +512 -0
  320. package/src/lab/automation/planner.ts +371 -0
  321. package/src/lab/automation/policy.ts +136 -0
  322. package/src/lab/automation/queue.ts +191 -0
  323. package/src/lab/automation/recovery.ts +24 -0
  324. package/src/lab/automation/route-context.ts +21 -0
  325. package/src/lab/automation/run-key.ts +44 -0
  326. package/src/lab/automation/runs-query.ts +34 -0
  327. package/src/lab/automation/types.ts +160 -0
  328. package/src/lab/conformance/assertion.ts +325 -0
  329. package/src/lab/conformance/digest.ts +22 -0
  330. package/src/lab/conformance/executor.ts +741 -0
  331. package/src/lab/conformance/fixture-provider.ts +27 -0
  332. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  333. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  334. package/src/lab/conformance/harness-budget.ts +47 -0
  335. package/src/lab/conformance/index.ts +5 -0
  336. package/src/lab/conformance/jcs.ts +64 -0
  337. package/src/lab/conformance/json-pointer.ts +39 -0
  338. package/src/lab/conformance/manifest.ts +180 -0
  339. package/src/lab/conformance/mcp-stub.ts +179 -0
  340. package/src/lab/conformance/negative-controls.ts +164 -0
  341. package/src/lab/conformance/observation.ts +355 -0
  342. package/src/lab/conformance/runner.ts +68 -0
  343. package/src/lab/conformance/sse-normalize.ts +59 -0
  344. package/src/lab/conformance/suite-manifest.ts +78 -0
  345. package/src/lab/conformance/types.ts +214 -0
  346. package/src/lab/constants.ts +126 -0
  347. package/src/lab/digest.ts +64 -0
  348. package/src/lab/events/errors.ts +9 -0
  349. package/src/lab/events/limits.ts +117 -0
  350. package/src/lab/events/types.ts +229 -0
  351. package/src/lab/events/validate.ts +781 -0
  352. package/src/lab/fabric/constants.ts +40 -0
  353. package/src/lab/fabric/executor.ts +492 -0
  354. package/src/lab/fabric/index.ts +80 -0
  355. package/src/lab/fabric/manifest.ts +222 -0
  356. package/src/lab/fabric/observe.ts +489 -0
  357. package/src/lab/fabric/patch.ts +79 -0
  358. package/src/lab/fabric/producer-child.ts +139 -0
  359. package/src/lab/fabric/producer-isolate.ts +276 -0
  360. package/src/lab/fabric/producer-protocol.ts +61 -0
  361. package/src/lab/fabric/scratch.ts +439 -0
  362. package/src/lab/fabric/subject.ts +106 -0
  363. package/src/lab/fabric/types.ts +134 -0
  364. package/src/lab/fabric/verifier.ts +98 -0
  365. package/src/lab/index.ts +54 -0
  366. package/src/lab/ledger/artifact-refs.ts +127 -0
  367. package/src/lab/ledger/invalidation.ts +136 -0
  368. package/src/lab/ledger/purge.ts +310 -0
  369. package/src/lab/ledger/store.ts +532 -0
  370. package/src/lab/live/credential-lease.ts +53 -0
  371. package/src/lab/live/destination.ts +155 -0
  372. package/src/lab/live/executor.ts +336 -0
  373. package/src/lab/live/inert-tools.ts +56 -0
  374. package/src/lab/live/manifest.ts +85 -0
  375. package/src/lab/live/mcp-loopback.ts +57 -0
  376. package/src/lab/live/runner.ts +19 -0
  377. package/src/lab/live/sandbox.ts +61 -0
  378. package/src/lab/live/suite-manifest.ts +41 -0
  379. package/src/lab/live/transport.ts +118 -0
  380. package/src/lab/live/types.ts +197 -0
  381. package/src/lab/observe/from-conformance.ts +301 -0
  382. package/src/lab/observe/from-live.ts +117 -0
  383. package/src/lab/paths.ts +153 -0
  384. package/src/lab/projection/rebuild.ts +495 -0
  385. package/src/lab/projection/schema.ts +135 -0
  386. package/src/lab/projection/verdicts.ts +474 -0
  387. package/src/lab/projection/verification.ts +412 -0
  388. package/src/lab/public/bundle.ts +217 -0
  389. package/src/lab/public/community-authority.ts +175 -0
  390. package/src/lab/public/community-files.ts +29 -0
  391. package/src/lab/public/community.ts +479 -0
  392. package/src/lab/public/file-safety.ts +155 -0
  393. package/src/lab/public/ids.ts +26 -0
  394. package/src/lab/public/index.ts +16 -0
  395. package/src/lab/public/mutation-lock.ts +424 -0
  396. package/src/lab/public/operator.ts +353 -0
  397. package/src/lab/public/origin-purge.ts +79 -0
  398. package/src/lab/public/origin.ts +203 -0
  399. package/src/lab/public/privacy.ts +143 -0
  400. package/src/lab/public/private-file.ts +261 -0
  401. package/src/lab/public/project.ts +124 -0
  402. package/src/lab/public/purge-test-fault.ts +21 -0
  403. package/src/lab/public/purge.ts +223 -0
  404. package/src/lab/public/registry.ts +44 -0
  405. package/src/lab/public/revocation.ts +252 -0
  406. package/src/lab/public/signature.ts +243 -0
  407. package/src/lab/public/storage.ts +105 -0
  408. package/src/lab/public/strict-json.ts +206 -0
  409. package/src/lab/public/time.ts +26 -0
  410. package/src/lab/public/types.ts +172 -0
  411. package/src/lab/public/validate.ts +391 -0
  412. package/src/lab/query/catalog.ts +101 -0
  413. package/src/lab/query/connection.ts +107 -0
  414. package/src/lab/query/constants.ts +4 -0
  415. package/src/lab/query/cursor.ts +132 -0
  416. package/src/lab/query/dto-map.ts +277 -0
  417. package/src/lab/query/errors.ts +22 -0
  418. package/src/lab/query/freshness.ts +53 -0
  419. package/src/lab/query/index.ts +45 -0
  420. package/src/lab/query/latest-observation.ts +59 -0
  421. package/src/lab/query/passive-production.ts +159 -0
  422. package/src/lab/query/queries.ts +444 -0
  423. package/src/lab/query/types.ts +266 -0
  424. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  425. package/src/lab/subject/installation-salt.ts +112 -0
  426. package/src/lab/subject/protocol-subject.ts +80 -0
  427. package/src/lab/subject/route-subject.ts +74 -0
  428. package/src/lib/admin-secrets.ts +24 -0
  429. package/src/lib/app-owned-memory-stores.ts +22 -0
  430. package/src/lib/bounded-body.ts +167 -11
  431. package/src/lib/bun-runtime.ts +125 -12
  432. package/src/lib/bun-stream-caps.ts +13 -9
  433. package/src/lib/codex-restart-contract.ts +120 -0
  434. package/src/lib/config-ownership.ts +26 -2
  435. package/src/lib/destination-policy.ts +65 -1
  436. package/src/lib/errors.ts +80 -5
  437. package/src/lib/fabric-task-execution-authority.ts +7 -0
  438. package/src/lib/fabric-task-host.ts +29 -0
  439. package/src/lib/lab-activation.ts +223 -0
  440. package/src/lib/lab-live-execution-authority.ts +13 -0
  441. package/src/lib/lab-live-host.ts +30 -0
  442. package/src/lib/lab-live-pinned-sender.ts +56 -0
  443. package/src/lib/lab-live-route-production.ts +130 -0
  444. package/src/lib/lab-passive-linker-registration.ts +26 -0
  445. package/src/lib/local-management-attestation.ts +51 -0
  446. package/src/lib/local-management-capability.ts +100 -0
  447. package/src/lib/local-provider-reload-contract.ts +100 -0
  448. package/src/lib/optional-shutdown-hooks.ts +57 -0
  449. package/src/lib/package-tree-integrity.ts +101 -0
  450. package/src/lib/pinned-http.ts +145 -26
  451. package/src/lib/process-control.ts +6 -2
  452. package/src/lib/provider-outbound.ts +49 -9
  453. package/src/lib/redact.ts +419 -3
  454. package/src/lib/self-launch-argv.ts +15 -0
  455. package/src/lib/server-resource-ownership.ts +71 -0
  456. package/src/lib/service-secrets.ts +15 -0
  457. package/src/lib/shadow-call.ts +35 -4
  458. package/src/lib/sse-decoder.ts +41 -0
  459. package/src/lib/state-store-registrations.ts +10 -2
  460. package/src/lib/system-restart-contract.ts +73 -0
  461. package/src/lib/token-estimate.ts +19 -2
  462. package/src/lib/tool-argument-integers.ts +253 -0
  463. package/src/lib/translator-budget.ts +44 -0
  464. package/src/lib/upstream-http-version.ts +57 -0
  465. package/src/lib/upstream-reachability.ts +95 -0
  466. package/src/lib/upstream-retry.ts +156 -3
  467. package/src/lib/windows-atomic-replace.ts +155 -0
  468. package/src/lib/windows-elevation.ts +70 -2
  469. package/src/lib/windows-secret-acl.ts +409 -69
  470. package/src/lib/windows-service-wrappers.ts +72 -0
  471. package/src/lib/windows-text.ts +106 -0
  472. package/src/lib/windows-user-principal.ts +341 -0
  473. package/src/lib/winsw.ts +33 -5
  474. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  475. package/src/oauth/account-import/index.ts +15 -0
  476. package/src/oauth/account-import/parser.ts +83 -0
  477. package/src/oauth/account-import/registry.ts +18 -0
  478. package/src/oauth/account-import/service.ts +75 -0
  479. package/src/oauth/account-import/types.ts +91 -0
  480. package/src/oauth/anthropic.ts +12 -1
  481. package/src/oauth/callback-server.ts +30 -4
  482. package/src/oauth/chatgpt.ts +12 -1
  483. package/src/oauth/cline.ts +203 -0
  484. package/src/oauth/command-code.ts +239 -0
  485. package/src/oauth/cursor.ts +46 -5
  486. package/src/oauth/generic-account-failover.ts +231 -0
  487. package/src/oauth/google-antigravity.ts +35 -3
  488. package/src/oauth/health.ts +20 -12
  489. package/src/oauth/index.ts +462 -71
  490. package/src/oauth/key-providers.ts +25 -0
  491. package/src/oauth/kimi.ts +25 -3
  492. package/src/oauth/kiro.ts +95 -6
  493. package/src/oauth/local-token-detect.ts +13 -2
  494. package/src/oauth/log.ts +3 -1
  495. package/src/oauth/login-cli.ts +88 -28
  496. package/src/oauth/nous.ts +798 -0
  497. package/src/oauth/open-browser-choice.ts +26 -0
  498. package/src/oauth/store.ts +133 -26
  499. package/src/oauth/token-guardian.ts +9 -3
  500. package/src/oauth/types.ts +15 -0
  501. package/src/pi/models.ts +2 -2
  502. package/src/providers/alibaba-region-migration.ts +1 -1
  503. package/src/providers/antigravity-models.ts +521 -31
  504. package/src/providers/auto-compact-budget.ts +65 -0
  505. package/src/providers/base-url-choices.ts +10 -0
  506. package/src/providers/codex-capacity.ts +292 -0
  507. package/src/providers/command-code-efforts.ts +176 -0
  508. package/src/providers/context-cap.ts +26 -8
  509. package/src/providers/cursor-pool.ts +72 -0
  510. package/src/providers/default-aliases.ts +65 -0
  511. package/src/providers/derive.ts +281 -6
  512. package/src/providers/fastwire.ts +507 -0
  513. package/src/providers/free-directory.ts +10 -7
  514. package/src/providers/google-vertex-location.ts +14 -0
  515. package/src/providers/key-failover.ts +71 -3
  516. package/src/providers/label.ts +35 -2
  517. package/src/providers/model-discovery-limits.ts +16 -0
  518. package/src/providers/model-discovery.ts +115 -22
  519. package/src/providers/model-presets.ts +119 -0
  520. package/src/providers/model-rename-migration.ts +255 -0
  521. package/src/providers/model-rename-startup.ts +28 -0
  522. package/src/providers/new-model-policy.ts +146 -0
  523. package/src/providers/openai-sidecar.ts +72 -4
  524. package/src/providers/openai-tier-startup.ts +31 -2
  525. package/src/providers/openai-tiers.ts +119 -4
  526. package/src/providers/openai-virtual-models.ts +1 -0
  527. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  528. package/src/providers/provider-id-rewrite.ts +30 -0
  529. package/src/providers/quota.ts +1379 -40
  530. package/src/providers/registry.ts +1545 -112
  531. package/src/providers/request-pacing.ts +310 -0
  532. package/src/providers/service-tier.ts +277 -0
  533. package/src/providers/slug-codec.ts +94 -6
  534. package/src/providers/static-model-discovery.ts +86 -0
  535. package/src/providers/xai-responses-opt-in.ts +15 -0
  536. package/src/providers/xai-transport.ts +32 -4
  537. package/src/reasoning-effort.ts +68 -3
  538. package/src/responses/apply-patch-envelope.ts +63 -0
  539. package/src/responses/code-mode-helper-compat.ts +50 -0
  540. package/src/responses/compaction.ts +26 -1
  541. package/src/responses/custom-tool-compat.ts +384 -0
  542. package/src/responses/hosted-tool-policy.ts +9 -0
  543. package/src/responses/namespace-tool-compat.ts +435 -0
  544. package/src/responses/parser.ts +225 -38
  545. package/src/responses/provider-continuation.ts +98 -0
  546. package/src/responses/provider-opaque-metadata.ts +73 -0
  547. package/src/responses/reasoning-envelope.ts +9 -1
  548. package/src/responses/reasoning-replay-cache.ts +504 -0
  549. package/src/responses/schema.ts +12 -2
  550. package/src/responses/spill-store.ts +75 -10
  551. package/src/responses/state.ts +659 -31
  552. package/src/responses/thought-signature-replay.ts +364 -0
  553. package/src/responses/tool-search-compat.ts +301 -0
  554. package/src/responses/truncated-stop-reason.ts +60 -0
  555. package/src/responses/turn-termination.ts +107 -0
  556. package/src/router.ts +405 -31
  557. package/src/routing/analytics.ts +379 -0
  558. package/src/routing/capability.ts +244 -0
  559. package/src/routing/compatibility/assemble.ts +73 -0
  560. package/src/routing/compatibility/behavior.ts +278 -0
  561. package/src/routing/compatibility/catalog.ts +99 -0
  562. package/src/routing/compatibility/endpoint.ts +52 -0
  563. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  564. package/src/routing/compatibility/policy.ts +181 -0
  565. package/src/routing/compatibility/provider-slot.ts +56 -0
  566. package/src/routing/compatibility/reader.ts +110 -0
  567. package/src/routing/compatibility/subject.ts +191 -0
  568. package/src/routing/compatibility/types.ts +64 -0
  569. package/src/routing/compatibility/version.ts +104 -0
  570. package/src/routing/cost.ts +77 -0
  571. package/src/routing/evaluator.ts +495 -0
  572. package/src/routing/health.ts +412 -0
  573. package/src/routing/history/cursor.ts +43 -0
  574. package/src/routing/history/indexer.ts +605 -0
  575. package/src/routing/history/schema.ts +72 -0
  576. package/src/routing/profile-namespace.ts +15 -0
  577. package/src/routing/profile.ts +547 -0
  578. package/src/routing/quota.ts +155 -0
  579. package/src/routing/request-evidence.ts +45 -0
  580. package/src/routing/trace.ts +776 -0
  581. package/src/server/adapter-resolve.ts +2 -29
  582. package/src/server/auth-cors.ts +289 -48
  583. package/src/server/background-lifecycle.ts +182 -0
  584. package/src/server/chat-completions.ts +154 -72
  585. package/src/server/chat-native-sse.ts +331 -0
  586. package/src/server/chat-native.ts +445 -0
  587. package/src/server/claude-messages.ts +160 -44
  588. package/src/server/direct-local-http.ts +347 -0
  589. package/src/server/effort-policy.ts +18 -0
  590. package/src/server/github-copilot-responses-repair.ts +338 -0
  591. package/src/server/gui-static.ts +39 -10
  592. package/src/server/images.ts +94 -12
  593. package/src/server/index.ts +1021 -190
  594. package/src/server/lifecycle.ts +336 -14
  595. package/src/server/live.ts +136 -17
  596. package/src/server/local-management-read-client.ts +90 -0
  597. package/src/server/local-provider-reload-client.ts +137 -0
  598. package/src/server/management/agent-settings-routes.ts +433 -116
  599. package/src/server/management/api-key-usage.ts +31 -5
  600. package/src/server/management/body.ts +6 -0
  601. package/src/server/management/codex-prompt-routes.ts +570 -0
  602. package/src/server/management/combo-routes.ts +64 -25
  603. package/src/server/management/config-routes.ts +511 -51
  604. package/src/server/management/context.ts +89 -2
  605. package/src/server/management/integration-routes.ts +498 -0
  606. package/src/server/management/lab-automation-routes.ts +206 -0
  607. package/src/server/management/lab-routes.ts +563 -0
  608. package/src/server/management/logs-usage-routes.ts +127 -52
  609. package/src/server/management/model-routes.ts +461 -133
  610. package/src/server/management/model-rows.ts +167 -0
  611. package/src/server/management/native-integration-routes.ts +770 -0
  612. package/src/server/management/oauth-account-routes.ts +90 -8
  613. package/src/server/management/provider-capability-config.ts +48 -0
  614. package/src/server/management/provider-routes.ts +836 -159
  615. package/src/server/management/request-history-routes.ts +191 -0
  616. package/src/server/management/route-registry.ts +332 -0
  617. package/src/server/management/routing-analytics-routes.ts +74 -0
  618. package/src/server/management/routing-profile-routes.ts +380 -0
  619. package/src/server/management/shared.ts +27 -11
  620. package/src/server/management/sidebar-routes.ts +47 -31
  621. package/src/server/management/storage-log-guard-routes.ts +186 -0
  622. package/src/server/management/sync-response.ts +69 -0
  623. package/src/server/management/system-restart.ts +289 -33
  624. package/src/server/management/system-routes.ts +77 -0
  625. package/src/server/management/usage-summary-cache.ts +9 -1
  626. package/src/server/management/vision-sidecar-options.ts +167 -0
  627. package/src/server/management/web-search-sidecar-options.ts +120 -0
  628. package/src/server/management-api.ts +126 -23
  629. package/src/server/management-auth.ts +220 -5
  630. package/src/server/passive-route-linker.ts +66 -0
  631. package/src/server/port-reclaim.ts +1 -1
  632. package/src/server/ports.ts +41 -1
  633. package/src/server/proxy-liveness.ts +161 -10
  634. package/src/server/readiness.ts +99 -0
  635. package/src/server/relay-eager.ts +221 -54
  636. package/src/server/relay.ts +479 -87
  637. package/src/server/request-decompress.ts +113 -6
  638. package/src/server/request-log-conversation.ts +51 -0
  639. package/src/server/request-log.ts +298 -31
  640. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  641. package/src/server/responses/agent-task-recovery.ts +465 -0
  642. package/src/server/responses/codex-auth-error.ts +55 -0
  643. package/src/server/responses/collaboration.ts +239 -36
  644. package/src/server/responses/combo-stream-preflight.ts +178 -0
  645. package/src/server/responses/compact.ts +444 -72
  646. package/src/server/responses/core.ts +3579 -436
  647. package/src/server/responses/empty-completion-guard.ts +333 -0
  648. package/src/server/responses/encrypted-payload.ts +62 -39
  649. package/src/server/responses/fetch-helpers.ts +89 -99
  650. package/src/server/responses/input-admission.ts +187 -0
  651. package/src/server/responses/pacing-overload.ts +13 -0
  652. package/src/server/responses/passthrough-error.ts +33 -9
  653. package/src/server/responses/policy-fallback.ts +178 -0
  654. package/src/server/responses/responses-field-backfill.ts +343 -0
  655. package/src/server/responses/terminal-guard.ts +26 -5
  656. package/src/server/responses/upstream-error.ts +5 -0
  657. package/src/server/responses/ws-upstream.ts +381 -0
  658. package/src/server/responses-custom-tool-repair.ts +363 -0
  659. package/src/server/responses-item-id-repair.ts +54 -6
  660. package/src/server/responses-json-events.ts +90 -0
  661. package/src/server/responses-model-rewrite.ts +29 -0
  662. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  663. package/src/server/responses-snapshot-repair.ts +621 -0
  664. package/src/server/responses-terminal-repair.ts +363 -0
  665. package/src/server/responses-tool-search-repair.ts +267 -0
  666. package/src/server/responses-undeclared-tool-guard.ts +376 -0
  667. package/src/server/responses.ts +18 -2
  668. package/src/server/search.ts +78 -13
  669. package/src/server/sse-frame-buffer.ts +319 -0
  670. package/src/server/sse-payload-rewrite.ts +110 -22
  671. package/src/server/startup-action-control.ts +8 -1
  672. package/src/server/startup-health-cache.ts +19 -1
  673. package/src/server/system-env.ts +80 -9
  674. package/src/server/ws-bridge.ts +53 -40
  675. package/src/service-manager-probe.ts +892 -0
  676. package/src/service.ts +1153 -94
  677. package/src/sidecar/auth.ts +92 -0
  678. package/src/sidecar/candidates.ts +83 -0
  679. package/src/storage/cleanup.ts +2 -2
  680. package/src/storage/policy-job.ts +14 -4
  681. package/src/storage/policy.ts +88 -23
  682. package/src/storage/scanner.ts +1 -1
  683. package/src/storage/worker-lifecycle.ts +14 -14
  684. package/src/tray/windows-tray.ps1 +83 -9
  685. package/src/tray/windows.ts +43 -16
  686. package/src/types/accounts.ts +37 -0
  687. package/src/types/config.ts +911 -0
  688. package/src/types/provider.ts +614 -0
  689. package/src/types/request.ts +397 -0
  690. package/src/types/tools.ts +236 -0
  691. package/src/types/wire.ts +80 -0
  692. package/src/types.ts +107 -1237
  693. package/src/update/index.ts +37 -23
  694. package/src/update/job.ts +445 -68
  695. package/src/update/notify.ts +12 -6
  696. package/src/update/npm-cache-preflight.d.mts +47 -0
  697. package/src/update/npm-cache-preflight.mjs +201 -0
  698. package/src/update/transactional-install.d.mts +22 -0
  699. package/src/update/transactional-install.mjs +266 -0
  700. package/src/usage/cost.ts +0 -0
  701. package/src/usage/expected-prices.ts +287 -16
  702. package/src/usage/log.ts +666 -50
  703. package/src/usage/summary.ts +388 -17
  704. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  705. package/src/usage/user-cost-overlays.ts +314 -0
  706. package/src/vision/anthropic-describe.ts +10 -6
  707. package/src/vision/backends.ts +97 -0
  708. package/src/vision/describe.ts +27 -16
  709. package/src/vision/eligibility.ts +250 -0
  710. package/src/vision/index.ts +238 -24
  711. package/src/vision/reasoning.ts +55 -0
  712. package/src/vision/routed-describe.ts +175 -0
  713. package/src/vision/timeout-bounds.ts +9 -0
  714. package/src/web-search/anthropic-executor.ts +13 -7
  715. package/src/web-search/backends.ts +108 -0
  716. package/src/web-search/exa-executor.ts +119 -0
  717. package/src/web-search/executor.ts +21 -6
  718. package/src/web-search/gemini-executor.ts +141 -0
  719. package/src/web-search/index.ts +150 -15
  720. package/src/web-search/loop.ts +284 -54
  721. package/src/web-search/parse.ts +125 -30
  722. package/src/web-search/sources.ts +60 -0
  723. package/src/web-search/xai-executor.ts +219 -0
  724. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  725. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  726. package/gui/dist/provider-icons/antigravity.svg +0 -1
  727. package/gui/dist/provider-icons/claude.svg +0 -1
  728. package/gui/dist/provider-icons/copilot.svg +0 -1
  729. package/gui/dist/provider-icons/cursor.svg +0 -2
  730. package/gui/dist/provider-icons/gemini.svg +0 -1
  731. package/gui/dist/provider-icons/grok-color.svg +0 -1
  732. package/gui/dist/provider-icons/kiro.svg +0 -14
  733. package/src/cli/internal-dispatch.ts +0 -20
@@ -1,7 +1,7 @@
1
1
  import http2 from "node:http2";
2
2
  import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
3
3
  import { namespacedToolName, type OcxProviderConfig, type OcxUsage } from "../../types";
4
- import { CONNECT_FLAG_END_STREAM, decodeAvailableConnectFrames, encodeConnectFrame } from "./framing";
4
+ import { CONNECT_FLAG_END_STREAM, ConnectFrameError, consumeConnectFrames, encodeConnectFrame } from "./framing";
5
5
  import {
6
6
  CURSOR_MAX_EFFECTIVE_CONNECT_PAYLOAD_BYTES,
7
7
  CURSOR_MAX_CONNECT_FRAME_BYTES,
@@ -13,6 +13,8 @@ import {
13
13
  type TranslatorBudget,
14
14
  } from "../../lib/translator-budget";
15
15
  import { activePromptText, prepareCursorRunRequest } from "./protobuf-request";
16
+ import { prepareCursorRawMessages, resolveActiveCursorImages } from "./images";
17
+ import { cursorRequestMessagesFromRaw } from "./request-builder";
16
18
  import {
17
19
  createCursorContextUsageTracker,
18
20
  createCursorProtobufEventState,
@@ -33,6 +35,7 @@ import {
33
35
  CreatePlanRequestResponseSchema,
34
36
  CreatePlanResultSchema,
35
37
  CreatePlanSuccessSchema,
38
+ ConversationStateStructureSchema,
36
39
  ExaFetchRequestResponseSchema,
37
40
  ExaFetchRequestResponse_ApprovedSchema,
38
41
  ExaSearchRequestResponseSchema,
@@ -48,7 +51,7 @@ import {
48
51
  type InteractionResponse,
49
52
  } from "./gen/agent_pb";
50
53
  import { debugProviderDiagnostic } from "../../lib/debug";
51
- import { classifyCursorError, isCursorBenignCancelError, safeCursorErrorMessage } from "./cursor-errors";
54
+ import { classifyCursorError, CursorUnexpectedCancelError, isCursorAbortError, isCursorBenignCancelError, safeCursorErrorMessage } from "./cursor-errors";
52
55
  import { mcpArgsFromToolCall } from "./protobuf-events";
53
56
  import { OCX_RESPONSES_TOOL_PROVIDER } from "./tool-definitions";
54
57
  import {
@@ -70,6 +73,7 @@ import {
70
73
  cursorToolArgNormalizeSchema,
71
74
  cursorToolWireName,
72
75
  cursorToolsForActivePrompt,
76
+ isCursorSyntheticStructuredEditTool,
73
77
  isGenericToolUseCountDemoPrompt,
74
78
  requestedCursorToolUseCount,
75
79
  } from "./tool-definitions";
@@ -80,11 +84,31 @@ import {
80
84
  } from "./native-exec-shell";
81
85
  import type { CursorClientMessage, CursorRunRequest, CursorServerMessage } from "./types";
82
86
  import type { CursorTransport, CursorTransportFactoryInput } from "./transport";
87
+ import { CursorHttp1BidiConnection } from "./http1-bidi";
88
+ import { isPinnedHttp1 } from "../../lib/upstream-http-version";
83
89
 
84
90
  const CURSOR_RUN_PATH = "/agent.v1.AgentService/Run";
85
91
  const CURSOR_CLIENT_VERSION = "cli-2026.07.08-0c04a8a";
86
92
  const HEARTBEAT_MS = 5_000;
87
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;
88
112
  const CURSOR_TIMEOUT_DESTROY_GRACE_MS = 1_000;
89
113
  const CLIENT_TOOL_FINALIZE_GRACE_MS = 50;
90
114
  const GENERIC_TOOL_COUNT_MIN_FINALIZE_GRACE_MS = 750;
@@ -391,6 +415,20 @@ export function finalizeAfterDrain(state: ReturnType<typeof createCursorProtobuf
391
415
  export function clientToolFinalizeGraceMsForRequest(request: CursorRunRequest, baseGraceMs = CLIENT_TOOL_FINALIZE_GRACE_MS): number {
392
416
  if (request.rawMessages?.at(-1)?.role === "toolResult") return baseGraceMs;
393
417
  const text = activePromptText(request);
418
+ // Parallel-tool requests with several advertised tools get the expanded window regardless of
419
+ // prompt shape: external models (grok) assemble sibling calls serially over multiple frames,
420
+ // and the 50ms drain grace ended the turn after 1-2 of them (devlog 260826_cursor_responses_gap,
421
+ // live 10-parallel probe: calls=2 then calls=1).
422
+ if (request.parallelToolCalls === true && (request.tools?.length ?? 0) > 1) {
423
+ const advertised = request.tools?.length ?? 0;
424
+ return Math.max(
425
+ baseGraceMs,
426
+ Math.min(
427
+ GENERIC_TOOL_COUNT_MAX_FINALIZE_GRACE_MS,
428
+ Math.max(GENERIC_TOOL_COUNT_MIN_FINALIZE_GRACE_MS, advertised * GENERIC_TOOL_COUNT_PER_TOOL_GRACE_MS),
429
+ ),
430
+ );
431
+ }
394
432
  if (!cursorRequestHasShellAlias(request.tools) || !isGenericToolUseCountDemoPrompt(text)) return baseGraceMs;
395
433
  const requestedCount = requestedCursorToolUseCount(text);
396
434
  const expandedGraceMs = requestedCount
@@ -405,10 +443,29 @@ export function clientToolFinalizeGraceMsForRequest(request: CursorRunRequest, b
405
443
  class LiveCursorTransport implements CursorTransport {
406
444
  private session?: http2.ClientHttp2Session;
407
445
  private stream?: http2.ClientHttp2Stream;
446
+ private http1Connection?: CursorHttp1BidiConnection;
408
447
  private heartbeat?: ReturnType<typeof setInterval>;
409
448
  private firstFrameTimer?: ReturnType<typeof setTimeout>;
449
+ private turnEndedCloseTimer?: ReturnType<typeof setTimeout>;
450
+ /**
451
+ * T04 inbound stream-health watchdog. Armed after the request is on the wire, reset by
452
+ * every DECODED frame (raw chunks deliberately do not count — TLS keepalive noise must not
453
+ * defeat it), disarmed by any settle/expected-close path. One timer covers both thresholds:
454
+ * it always fires at min(lastInbound + silence, lastMeaningful + heartbeatOnly) and re-arms
455
+ * when neither deadline has actually elapsed.
456
+ */
457
+ private streamHealthTimer?: ReturnType<typeof setTimeout>;
458
+ private lastInboundFrameAt = 0;
459
+ private lastMeaningfulFrameAt = 0;
460
+ private streamHealthFail?: (error: Error) => void;
410
461
  private committed = false;
411
462
  private expectedClose = false;
463
+ /**
464
+ * True once a terminal (`done` or `error`) has been admitted to the outbound queue. Read only
465
+ * by the EOF branch below: after a mapper error the bridge has already failed the turn, so
466
+ * failing again on EOF would add a duplicate adapter error for no benefit.
467
+ */
468
+ private emittedTerminal = false;
412
469
  private pendingFinalize?: ReturnType<typeof setTimeout>;
413
470
  private readonly clientToolFinalizeGraceMs: number;
414
471
  private activeClientToolFinalizeGraceMs: number;
@@ -427,12 +484,17 @@ class LiveCursorTransport implements CursorTransport {
427
484
  // close; safe to read after a stream failure because open() owns the only writer before run().
428
485
  private turnStartedAt = 0;
429
486
  private framesReceived = 0;
487
+ private sawAssistantText = false;
430
488
  private firstFrameAt?: number;
431
- private firstFrameLogged = false;
489
+ private firstFrameLogged = false;
432
490
  /** Stable session identifier sent as x-session-id; mirrors IDE session semantics. */
433
- private readonly sessionId = crypto.randomUUID();
491
+ private readonly sessionId: string;
492
+ /** Per-transport owner for native-exec / background shells. Must not share conversationId. */
493
+ private readonly shellOwnerId = crypto.randomUUID();
494
+ private capturedCheckpointBytes?: Uint8Array;
434
495
 
435
496
  constructor(private readonly input: CursorTransportFactoryInput) {
497
+ this.sessionId = input.sessionId?.trim() || crypto.randomUUID();
436
498
  this.translatorBudget = input.translatorBudget;
437
499
  this.token = resolveCursorToken(input.provider, input.headers);
438
500
  // Grace window before a drained client-tool turn is finalized. Small enough not to look like a
@@ -444,7 +506,7 @@ class LiveCursorTransport implements CursorTransport {
444
506
  this.desktopDeps = desktopDepsFromConfig(input.provider.desktopExecutor);
445
507
  this.execContext = {
446
508
  ...this.desktopDeps,
447
- sessionId: this.sessionId,
509
+ sessionId: this.shellOwnerId,
448
510
  unsafeAllowNativeLocalExec: effectiveCursorNativeExecAllow(input.provider, input.requestDeclaresFullAccess === true),
449
511
  };
450
512
  const servers = resolveMcpServers(input.provider);
@@ -479,7 +541,7 @@ class LiveCursorTransport implements CursorTransport {
479
541
  ...this.desktopDeps,
480
542
  ...mcpDepsFromManager(this.mcpManager!),
481
543
  mcpToolDefs,
482
- sessionId: this.sessionId,
544
+ sessionId: this.shellOwnerId,
483
545
  unsafeAllowNativeLocalExec: effectiveCursorNativeExecAllow(this.input.provider, this.input.requestDeclaresFullAccess === true),
484
546
  };
485
547
  } catch (err) {
@@ -521,6 +583,22 @@ class LiveCursorTransport implements CursorTransport {
521
583
  }
522
584
  return err;
523
585
  };
586
+ /**
587
+ * A cancel we did not request is a real transport failure, but as a raw `NGHTTP2_CANCEL` it
588
+ * gets swallowed twice over: the adapter re-decides "benign" from the error code alone
589
+ * (`cursor.ts:181`) and drops the turn, and any message that survives is re-matched
590
+ * downstream and labelled an intentional "Cursor stream suspended". Raising a typed error
591
+ * carries the provenance this class already holds.
592
+ *
593
+ * Suppressed once a terminal was emitted: the turn already ended, and a second terminal flips
594
+ * a completed buffered response to failed.
595
+ */
596
+ const classifyTurnFailure = (err: Error): Error => {
597
+ if (!this.expectedClose && !this.emittedTerminal && isCursorBenignCancelError(err)) {
598
+ return summarizeFailure(new CursorUnexpectedCancelError(err));
599
+ }
600
+ return summarizeFailure(err);
601
+ };
524
602
  const wake = () => {
525
603
  const fn = notify;
526
604
  notify = undefined;
@@ -530,20 +608,54 @@ class LiveCursorTransport implements CursorTransport {
530
608
  const push = (message: CursorServerMessage) => {
531
609
  const bytes = new TextEncoder().encode(JSON.stringify(message)).byteLength;
532
610
  this.reserveTransportBytes(bytes);
611
+ if (message.type === "done" || message.type === "error") this.emittedTerminal = true;
533
612
  queue.push({ message, bytes });
534
613
  wake();
535
614
  };
536
615
 
537
616
  // Advertise MCP tools before the stream opens — the server only calls tools it was told about.
538
617
  await this.prepareMcp();
539
- const activeText = activePromptText(request);
540
- this.activeClientToolFinalizeGraceMs = clientToolFinalizeGraceMsForRequest(request, this.clientToolFinalizeGraceMs);
541
- const cursorVisibleTools = cursorToolsForActivePrompt(request.tools, activeText, request.toolChoice);
542
- const clientToolDefs = buildCursorToolDefinitions(cursorVisibleTools, request.toolChoice);
618
+ // JPEG soft-cap rewrite for active-turn data: images before encode. Rebuild text
619
+ // messages from the prepared raw channel so omission markers replace stale
620
+ // pre-rewrite content that activePromptText and the tool filter would otherwise see.
621
+ const preparedRaw = await prepareCursorRawMessages(request.rawMessages, signal);
622
+ const preparedRawMessages = preparedRaw.messages;
623
+ const selectedImages = await resolveActiveCursorImages(
624
+ preparedRawMessages,
625
+ signal,
626
+ preparedRaw.images,
627
+ );
628
+ const preparedMessages = preparedRawMessages === request.rawMessages
629
+ ? request.messages
630
+ : cursorRequestMessagesFromRaw(preparedRawMessages);
631
+ const activeRequest: CursorRunRequest = {
632
+ ...request,
633
+ messages: preparedMessages,
634
+ rawMessages: preparedRawMessages,
635
+ selectedImages,
636
+ };
637
+ const activeText = activePromptText(activeRequest);
638
+ this.activeClientToolFinalizeGraceMs = clientToolFinalizeGraceMsForRequest(activeRequest, this.clientToolFinalizeGraceMs);
639
+ const cursorVisibleTools = cursorToolsForActivePrompt(activeRequest.tools, activeText, activeRequest.toolChoice);
640
+ const clientToolDefs = buildCursorToolDefinitions(cursorVisibleTools, activeRequest.toolChoice);
641
+ // `request.tools` is the catalog already filtered and budgeted by request-builder. Derive
642
+ // conversion provenance only from tagged synthetic tools that also survive this final prompt
643
+ // filter; a client tool with the same wire name can never opt into conversion by collision.
644
+ const syntheticStructuredEditToolNames = new Set(
645
+ (cursorVisibleTools ?? [])
646
+ .filter(isCursorSyntheticStructuredEditTool)
647
+ .map(cursorToolWireName),
648
+ );
649
+ const freeformToolNames = new Set(
650
+ (cursorVisibleTools ?? [])
651
+ .filter(tool => tool.freeform)
652
+ .map(tool => namespacedToolName(tool.namespace, tool.name)),
653
+ );
543
654
  this.execContext = {
544
655
  ...this.execContext,
545
656
  clientToolDefs,
546
657
  rejectNativeFileMutations: cursorRequestAdvertisesApplyPatch(request.tools, request.toolChoice),
658
+ structuredEditAvailable: syntheticStructuredEditToolNames.size > 0,
547
659
  };
548
660
  const toolSchemas = new Map<string, unknown>();
549
661
  const cursorToolNameMap = new Map<string, string>();
@@ -561,16 +673,18 @@ class LiveCursorTransport implements CursorTransport {
561
673
  });
562
674
  // Build the payload once. The estimate is only worth deriving when there is no
563
675
  // carry-forward to fall back on — with a carry present it would never be used (#373).
564
- const prepared = prepareCursorRunRequest(request, {
676
+ const prepared = prepareCursorRunRequest(activeRequest, {
565
677
  estimateInputTokens: contextUsage.carryForwardTokens === undefined,
566
678
  });
567
679
  this.blobRequestScope = prepared.blobRequestScope;
568
680
  try {
569
681
  state = createCursorProtobufEventState({
570
682
  clientToolNames: clientToolDefs.map(tool => tool.toolName || tool.name),
683
+ freeformToolNames,
571
684
  parallelToolCalls: request.parallelToolCalls,
572
685
  toolSchemas,
573
686
  cursorToolNameMap,
687
+ syntheticStructuredEditToolNames,
574
688
  translatorBudget: this.translatorBudget,
575
689
  contextUsage,
576
690
  ...(prepared.estimatedInputTokens !== undefined
@@ -603,7 +717,19 @@ class LiveCursorTransport implements CursorTransport {
603
717
  // A CANCEL is benign only on the client-tool suspend path (expectedClose); an
604
718
  // unexpected server-side NGHTTP2_CANCEL must surface as a real transport error.
605
719
  if (this.expectedClose && isCursorBenignCancelError(failure)) return;
606
- throw attachPartialUsage(summarizeFailure(failure), state);
720
+ // A teardown error arriving AFTER the turn's terminal frame describes the connection,
721
+ // not the turn: the answer is committed and every queued message has been yielded.
722
+ //
723
+ // Narrow on purpose. A benign cancel after a terminal is already swallowed one layer
724
+ // up (`cursor.ts:183`), so widening this to every post-terminal error would change
725
+ // what the adapter sees for genuine faults. What it does cover is the abort case
726
+ // from #1527: `signal.abort` fires `failAndClear(new Error("Cursor request was
727
+ // aborted"))`, which is NOT benign (`cursor-errors.ts:74`), so an ordinary completed
728
+ // turn that is then torn down still surfaced as `turn-failed` with
729
+ // `expectedClose:false`. Only `cancelCursorRun()` sets `expectedClose`, so a normal
730
+ // completion never qualified for the branch above.
731
+ if (this.emittedTerminal && isCursorAbortError(failure)) return;
732
+ throw attachPartialUsage(classifyTurnFailure(failure), state);
607
733
  }
608
734
  if (done) break;
609
735
  await new Promise<void>(resolve => {
@@ -612,7 +738,8 @@ class LiveCursorTransport implements CursorTransport {
612
738
  }
613
739
  if (failure) {
614
740
  if (this.expectedClose && isCursorBenignCancelError(failure)) return;
615
- throw attachPartialUsage(summarizeFailure(failure), state);
741
+ if (this.emittedTerminal && isCursorAbortError(failure)) return;
742
+ throw attachPartialUsage(classifyTurnFailure(failure), state);
616
743
  }
617
744
  }
618
745
 
@@ -639,12 +766,24 @@ class LiveCursorTransport implements CursorTransport {
639
766
  || this.pendingTransportFrames >= CURSOR_MAX_PENDING_FRAMES
640
767
  ) {
641
768
  this.stream?.pause();
769
+ this.http1Connection?.pause();
642
770
  return;
643
771
  }
644
772
  if (
645
773
  this.transportBufferedBytes <= CURSOR_TRANSPORT_RESUME_BYTES
646
774
  && this.pendingTransportFrames <= CURSOR_PENDING_FRAMES_RESUME
647
- ) this.stream?.resume();
775
+ ) {
776
+ this.stream?.resume();
777
+ this.http1Connection?.resume();
778
+ }
779
+ }
780
+
781
+ private writeConnectFrame(frame: Uint8Array): void {
782
+ if (this.http1Connection) {
783
+ this.http1Connection.write(frame);
784
+ return;
785
+ }
786
+ this.stream?.write(frame);
648
787
  }
649
788
 
650
789
  requestCommitted(): boolean {
@@ -658,16 +797,103 @@ class LiveCursorTransport implements CursorTransport {
658
797
  }
659
798
  }
660
799
 
800
+ private clearStreamHealthTimer(): void {
801
+ if (this.streamHealthTimer) {
802
+ clearTimeout(this.streamHealthTimer);
803
+ this.streamHealthTimer = undefined;
804
+ }
805
+ this.streamHealthFail = undefined;
806
+ }
807
+
808
+ /**
809
+ * T04: arm (or re-arm) the inbound stream-health watchdog. `fail` is the turn's
810
+ * failAndClear; the timer owns nothing else. Never armed before the first decoded
811
+ * frame (the first-frame timer covers dial + first response), and disarmed by
812
+ * every settle / expected-close path alongside the other timers.
813
+ */
814
+ private armStreamHealthTimer(fail: (error: Error) => void): void {
815
+ if (this.streamHealthTimer) clearTimeout(this.streamHealthTimer);
816
+ if (this.expectedClose) return;
817
+ this.streamHealthFail = fail;
818
+ const silenceMs = this.input.streamSilenceFailMs ?? CURSOR_STREAM_SILENCE_FAIL_MS;
819
+ const heartbeatOnlyMs = this.input.streamHeartbeatOnlyFailMs ?? CURSOR_STREAM_HEARTBEAT_ONLY_FAIL_MS;
820
+ const now = Date.now();
821
+ const deadline = Math.min(
822
+ this.lastInboundFrameAt + silenceMs,
823
+ this.lastMeaningfulFrameAt + heartbeatOnlyMs,
824
+ );
825
+ this.streamHealthTimer = setTimeout(() => {
826
+ this.streamHealthTimer = undefined;
827
+ const failFn = this.streamHealthFail;
828
+ if (!failFn || this.expectedClose) return;
829
+ const stalledFor = Date.now() - this.lastInboundFrameAt;
830
+ const meaningfulStalledFor = Date.now() - this.lastMeaningfulFrameAt;
831
+ if (stalledFor < silenceMs && meaningfulStalledFor < heartbeatOnlyMs) {
832
+ // A frame landed between arming and firing — re-arm for the fresh deadline.
833
+ this.armStreamHealthTimer(failFn);
834
+ return;
835
+ }
836
+ const heartbeatOnly = stalledFor < silenceMs;
837
+ debugProviderDiagnostic("cursor", "stream-health-timeout", {
838
+ stalledMs: stalledFor,
839
+ meaningfulStalledMs: meaningfulStalledFor,
840
+ heartbeatOnly,
841
+ framesReceived: this.framesReceived,
842
+ elapsedMs: Date.now() - this.turnStartedAt,
843
+ });
844
+ const reason = heartbeatOnly
845
+ ? `Cursor stream stalled: heartbeat-only traffic for ${Math.round(meaningfulStalledFor / 1000)}s without turn progress`
846
+ : `Cursor stream stalled: no inbound frames for ${Math.round(stalledFor / 1000)}s before turnEnded`;
847
+ failFn(new Error(reason));
848
+ try { this.stream?.close(); } catch { this.stream?.destroy(); }
849
+ this.session?.close();
850
+ this.http1Connection?.close();
851
+ }, Math.max(0, deadline - now));
852
+ }
853
+
854
+ /**
855
+ * T04: record a decoded inbound frame. Liveness-only frames (server heartbeat,
856
+ * conversationCheckpointUpdate) keep the silence clock fresh but not the progress
857
+ * clock — matching senpi's split so a server that only pings still fails at the
858
+ * heartbeat-only threshold.
859
+ */
860
+ private noteInboundFrame(livenessOnly: boolean): void {
861
+ const now = Date.now();
862
+ this.lastInboundFrameAt = now;
863
+ if (!livenessOnly) this.lastMeaningfulFrameAt = now;
864
+ if (this.streamHealthFail) this.armStreamHealthTimer(this.streamHealthFail);
865
+ }
866
+
867
+ /**
868
+ * A clean Connect END_STREAM owns the turn terminal even when Cursor keeps the
869
+ * HTTP body open or tears it down with an abort/reset immediately afterward.
870
+ * Stop client-side liveness work and classify that later transport close as
871
+ * expected without actively sending an RST_STREAM back to Cursor.
872
+ */
873
+ private markProtocolComplete(): void {
874
+ this.expectedClose = true;
875
+ this.clearPendingFinalize();
876
+ if (this.heartbeat) {
877
+ clearInterval(this.heartbeat);
878
+ this.heartbeat = undefined;
879
+ }
880
+ this.clearFirstFrameTimer();
881
+ this.clearStreamHealthTimer();
882
+ }
883
+
661
884
  private startShellCleanup(): Promise<BackgroundShellTerminationReport> {
662
- return this.shellCleanup ??= terminateBackgroundShellsForSession(this.sessionId);
885
+ return this.shellCleanup ??= terminateBackgroundShellsForSession(this.shellOwnerId);
663
886
  }
664
887
 
665
888
  async close(): Promise<void> {
666
889
  if (this.heartbeat) clearInterval(this.heartbeat);
890
+ if (this.turnEndedCloseTimer) clearTimeout(this.turnEndedCloseTimer);
667
891
  this.clearPendingFinalize();
668
892
  this.clearFirstFrameTimer();
893
+ this.clearStreamHealthTimer();
669
894
  this.stream?.close();
670
895
  this.session?.close();
896
+ this.http1Connection?.close();
671
897
  this.releaseBlobRequestScope();
672
898
  this.releaseMcpObservation?.();
673
899
  this.releaseMcpObservation = undefined;
@@ -680,12 +906,17 @@ class LiveCursorTransport implements CursorTransport {
680
906
  this.clearPendingFinalize();
681
907
  if (this.heartbeat) clearInterval(this.heartbeat);
682
908
  this.clearFirstFrameTimer();
683
- try {
684
- this.stream?.close(http2.constants.NGHTTP2_CANCEL);
685
- } catch {
686
- this.stream?.destroy();
909
+ this.clearStreamHealthTimer();
910
+ if (this.http1Connection) {
911
+ this.http1Connection.close();
912
+ } else {
913
+ try {
914
+ this.stream?.close(http2.constants.NGHTTP2_CANCEL);
915
+ } catch {
916
+ this.stream?.destroy();
917
+ }
918
+ this.session?.close();
687
919
  }
688
- this.session?.close();
689
920
  this.releaseBlobRequestScope();
690
921
  this.releaseMcpObservation?.();
691
922
  this.releaseMcpObservation = undefined;
@@ -693,6 +924,46 @@ class LiveCursorTransport implements CursorTransport {
693
924
  void this.startShellCleanup().catch(() => { /* close() observes the same cleanup promise */ });
694
925
  }
695
926
 
927
+ /**
928
+ * T03 (#1062): after the server sends `turnEnded`, the application turn is complete.
929
+ * A server that keeps the HTTP/2 stream open past this point cannot hold the turn
930
+ * hostage until a 300s bridge idle timeout. Close our side after a short grace so any
931
+ * trailing frames (late usage, checkpoint) still land before we release the socket.
932
+ */
933
+ private closeAfterTurnEnded(): void {
934
+ if (this.turnEndedCloseTimer) return;
935
+ // The application turn is over: the T03 grace timer owns the socket from here.
936
+ // The T04 watchdog must disarm NOW, not at the grace close — a watchdog shorter
937
+ // than the grace would otherwise fail a completed turn.
938
+ this.clearStreamHealthTimer();
939
+ this.turnEndedCloseTimer = setTimeout(() => {
940
+ this.turnEndedCloseTimer = undefined;
941
+ // Only expectedClose (client-tool suspend cancel) blocks the close.
942
+ // emittedTerminal is intentionally NOT checked here: finalizeTurnEvents sets it
943
+ // synchronously during turnEnded mapping, ~500ms before this timer fires, so
944
+ // checking it would make the close unreachable on every real path (the exact
945
+ // scenario this PR exists to fix — senpi #1062).
946
+ if (this.expectedClose) return;
947
+ debugProviderDiagnostic("cursor", "turn-ended-close", {
948
+ committed: this.committed,
949
+ framesReceived: this.framesReceived,
950
+ });
951
+ this.expectedClose = true;
952
+ this.clearFirstFrameTimer();
953
+ this.clearStreamHealthTimer();
954
+ if (this.heartbeat) clearInterval(this.heartbeat);
955
+ if (this.http1Connection) {
956
+ this.http1Connection.close();
957
+ } else {
958
+ try {
959
+ this.stream?.close();
960
+ } catch {
961
+ this.stream?.destroy();
962
+ }
963
+ }
964
+ }, TURN_ENDED_CLOSE_GRACE_MS);
965
+ }
966
+
696
967
  private releaseBlobRequestScope(): void {
697
968
  const scope = this.blobRequestScope;
698
969
  if (!scope) return;
@@ -750,33 +1021,52 @@ class LiveCursorTransport implements CursorTransport {
750
1021
  fail: (error: Error) => void,
751
1022
  finish: () => void,
752
1023
  ): void {
1024
+ if (signal?.aborted) {
1025
+ fail(signal.reason instanceof Error ? signal.reason : new Error("Cursor request was aborted"));
1026
+ return;
1027
+ }
753
1028
  this.turnStartedAt = Date.now();
754
1029
  this.framesReceived = 0;
1030
+ this.sawAssistantText = false;
1031
+ this.emittedTerminal = false;
755
1032
  this.firstFrameAt = undefined;
756
1033
  this.firstFrameLogged = false;
757
- const dialHost = cursorHostLabel(this.input.provider.baseUrl || "https://api2.cursor.sh");
758
- debugProviderDiagnostic("cursor", "dial", { host: dialHost });
759
- this.session = http2.connect(this.input.provider.baseUrl || "https://api2.cursor.sh");
760
- // The run request is buffered until the HTTP/2 session connects. Failures before `connect`
761
- // (DNS, ECONNREFUSED, TLS, connect timeout) mean the server never received the request, so they
762
- // are safe to retry. Once connected, bytes flush to the server and the turn must not be replayed.
763
- this.session.on("connect", () => {
764
- this.committed = true;
765
- debugProviderDiagnostic("cursor", "connected", { connectMs: Date.now() - this.turnStartedAt });
766
- });
767
- this.stream = this.session.request({
768
- ":method": "POST",
769
- ":path": CURSOR_RUN_PATH,
770
- "content-type": "application/connect+proto",
771
- "connect-protocol-version": "1",
772
- te: "trailers",
773
- authorization: `Bearer ${this.token}`,
774
- "x-ghost-mode": "true",
775
- "x-cursor-client-version": CURSOR_CLIENT_VERSION,
776
- "x-cursor-client-type": "cli",
777
- "x-request-id": crypto.randomUUID(),
778
- "x-session-id": this.sessionId,
779
- });
1034
+ const baseUrl = this.input.provider.baseUrl || "https://api2.cursor.sh";
1035
+ const useHttp1 = isPinnedHttp1(this.input.provider.upstreamHttpVersion);
1036
+ const requestId = crypto.randomUUID();
1037
+ const dialHost = cursorHostLabel(baseUrl);
1038
+ debugProviderDiagnostic("cursor", "dial", { host: dialHost, transport: useHttp1 ? "http1.1" : "http2" });
1039
+
1040
+ let session: http2.ClientHttp2Session | undefined;
1041
+ let stream: http2.ClientHttp2Stream | undefined;
1042
+ if (!useHttp1) {
1043
+ session = http2.connect(baseUrl);
1044
+ this.session = session;
1045
+ // The run request is buffered until the HTTP/2 session connects. Failures before `connect`
1046
+ // (DNS, ECONNREFUSED, TLS, connect timeout) mean the server never received the request, so they
1047
+ // are safe to retry. Once connected, bytes flush to the server and the turn must not be replayed.
1048
+ session.on("connect", () => {
1049
+ this.committed = true;
1050
+ debugProviderDiagnostic("cursor", "connected", {
1051
+ transport: "http2",
1052
+ connectMs: Date.now() - this.turnStartedAt,
1053
+ });
1054
+ });
1055
+ stream = session.request({
1056
+ ":method": "POST",
1057
+ ":path": CURSOR_RUN_PATH,
1058
+ "content-type": "application/connect+proto",
1059
+ "connect-protocol-version": "1",
1060
+ te: "trailers",
1061
+ authorization: `Bearer ${this.token}`,
1062
+ "x-ghost-mode": "true",
1063
+ "x-cursor-client-version": CURSOR_CLIENT_VERSION,
1064
+ "x-cursor-client-type": "cli",
1065
+ "x-request-id": requestId,
1066
+ "x-session-id": this.sessionId,
1067
+ });
1068
+ this.stream = stream;
1069
+ }
780
1070
 
781
1071
  // Single-shot terminal owner for this turn (createTerminalSettler): stream error, session
782
1072
  // error, trailers, end, abort, and the first-frame timeout all race into it, and only the
@@ -784,9 +1074,13 @@ class LiveCursorTransport implements CursorTransport {
784
1074
  const settler = createTerminalSettler({
785
1075
  fail,
786
1076
  finish,
787
- clearTimer: () => this.clearFirstFrameTimer(),
1077
+ clearTimer: () => {
1078
+ this.clearFirstFrameTimer();
1079
+ this.clearStreamHealthTimer();
1080
+ },
788
1081
  });
789
1082
  const failAndClear = (error: Error) => {
1083
+ releaseBacklogLease();
790
1084
  if (this.expectedClose) {
791
1085
  // We already emitted a terminal `done` and cancelled the run (client-tool suspension). The
792
1086
  // RST_STREAM CANCEL surfaces here as a stream error/abort; it is expected, not a failure.
@@ -801,11 +1095,9 @@ class LiveCursorTransport implements CursorTransport {
801
1095
  }
802
1096
  settler.settleFail(error);
803
1097
  };
804
- const session = this.session;
805
- const stream = this.stream;
806
1098
  // Session-level errors (TLS/socket/GOAWAY) do not always propagate to the stream listener;
807
1099
  // without this handler they could bypass orderly failure reporting entirely.
808
- session.on("error", err => {
1100
+ const onSessionError = (err: unknown) => {
809
1101
  const realErr = err instanceof Error ? err : new Error(String(err));
810
1102
  debugProviderDiagnostic("cursor", "session-error", {
811
1103
  code: String((realErr as { code?: unknown }).code ?? ""),
@@ -813,37 +1105,65 @@ class LiveCursorTransport implements CursorTransport {
813
1105
  elapsedMs: Date.now() - this.turnStartedAt,
814
1106
  });
815
1107
  failAndClear(realErr);
816
- });
1108
+ };
817
1109
  this.firstFrameTimer = setTimeout(() => {
818
1110
  this.firstFrameTimer = undefined;
819
1111
  debugProviderDiagnostic("cursor", "first-frame-timeout", { timeoutMs: this.input.firstFrameTimeoutMs ?? CURSOR_FIRST_FRAME_TIMEOUT_MS });
820
- try { stream.close(); } catch { /* already closing */ }
821
- try { session.close(); } catch { /* already closing */ }
822
- // close() waits for in-flight frames; a dead socket can ignore it — force-destroy shortly
823
- // after so a stalled TLS session cannot linger past the timeout.
824
- armTimeoutDestroyFallback(stream, session, this.input.timeoutDestroyGraceMs ?? CURSOR_TIMEOUT_DESTROY_GRACE_MS);
1112
+ try { stream?.close(); } catch { /* already closing */ }
1113
+ try { session?.close(); } catch { /* already closing */ }
1114
+ this.http1Connection?.close();
1115
+ if (stream && session) {
1116
+ // close() waits for in-flight frames; a dead socket can ignore it — force-destroy shortly
1117
+ // after so a stalled TLS session cannot linger past the timeout.
1118
+ armTimeoutDestroyFallback(stream, session, this.input.timeoutDestroyGraceMs ?? CURSOR_TIMEOUT_DESTROY_GRACE_MS);
1119
+ }
1120
+ releaseBacklogLease();
825
1121
  settler.settleFail(new Error("Cursor transport timed out before first response"));
826
1122
  }, this.input.firstFrameTimeoutMs ?? CURSOR_FIRST_FRAME_TIMEOUT_MS);
827
1123
 
828
- let pending: Uint8Array<ArrayBufferLike> = new Uint8Array();
829
- let frameWork: Promise<void> = Promise.resolve();
830
- const reservePayloadCopy = (bytes: number) => {
831
- if (this.transportBufferedBytes + bytes > CURSOR_TRANSPORT_MAX_BUFFERED_BYTES) {
832
- throw new TranslatorBudgetExceededError("cursor_transport", CURSOR_TRANSPORT_MAX_BUFFERED_BYTES);
1124
+ // Raw Connect backlog with a parse cursor: appends copy only the incoming
1125
+ // chunk (amortized capacity growth), the consumed prefix is reclaimed
1126
+ // lazily, and the RAW used length (headers included) is what the 32 MiB
1127
+ // transport cap bounds payload-only accounting let tiny-frame/header
1128
+ // floods slip through.
1129
+ let backlog = new Uint8Array();
1130
+ let backlogStart = 0;
1131
+ let backlogEnd = 0;
1132
+ const BACKLOG_COMPACT_MIN_SAVINGS = 64 * 1024;
1133
+ const appendBacklog = (chunk: Uint8Array): void => {
1134
+ const used = backlogEnd - backlogStart;
1135
+ let start = backlogStart;
1136
+ let end = backlogEnd;
1137
+ // Reclaim the consumed prefix when it is large or needed for capacity.
1138
+ if (start > 0 && (start >= BACKLOG_COMPACT_MIN_SAVINGS || end + chunk.byteLength > backlog.byteLength)) {
1139
+ backlog = backlog.slice(start, end);
1140
+ start = 0;
1141
+ end = used;
833
1142
  }
834
- const reservation = this.translatorBudget.reserveTransient(bytes, { kind: "cursor_transport" });
835
- this.transportBufferedBytes += bytes;
836
- this.updateTransportFlowControl();
837
- return {
838
- commitRetained: () => reservation.commitRetained(),
839
- release: () => {
840
- reservation.release();
841
- this.transportBufferedBytes = Math.max(0, this.transportBufferedBytes - bytes);
842
- this.updateTransportFlowControl();
843
- },
844
- };
1143
+ if (end + chunk.byteLength > backlog.byteLength) {
1144
+ const capacity = Math.max(8192, backlog.byteLength * 2, end + chunk.byteLength);
1145
+ const next = new Uint8Array(Math.min(CURSOR_TRANSPORT_MAX_BUFFERED_BYTES, capacity));
1146
+ next.set(backlog.subarray(start, end), 0);
1147
+ backlog = next;
1148
+ }
1149
+ backlog.set(chunk, end);
1150
+ backlogStart = start;
1151
+ backlogEnd = end + chunk.byteLength;
845
1152
  };
846
- const handleFrame = async (frame: ReturnType<typeof decodeAvailableConnectFrames>["frames"][number]) => {
1153
+ let frameWork: Promise<void> = Promise.resolve();
1154
+ // Idempotent terminal owner for the backlog lease: every settle/close path
1155
+ // must leave the raw charge at zero instead of relying on budget disposal.
1156
+ let backlogLeaseReleased = false;
1157
+ const releaseBacklogLease = () => {
1158
+ if (backlogLeaseReleased) return;
1159
+ backlogLeaseReleased = true;
1160
+ const leftover = backlogEnd - backlogStart;
1161
+ if (leftover > 0) this.releaseTransportBytes(leftover);
1162
+ backlog = new Uint8Array();
1163
+ backlogStart = 0;
1164
+ backlogEnd = 0;
1165
+ };
1166
+ const handleFrame = async (frame: ReturnType<typeof consumeConnectFrames>["frames"][number]) => {
847
1167
  this.framesReceived++;
848
1168
  if ((frame.flags & CONNECT_FLAG_END_STREAM) === CONNECT_FLAG_END_STREAM) {
849
1169
  const endError = parseConnectEndStreamError(frame.payload);
@@ -854,29 +1174,76 @@ class LiveCursorTransport implements CursorTransport {
854
1174
  framesReceived: this.framesReceived,
855
1175
  elapsedMs: Date.now() - this.turnStartedAt,
856
1176
  } : { framesReceived: this.framesReceived, elapsedMs: Date.now() - this.turnStartedAt });
857
- if (endError) failAndClear(endError);
1177
+ if (endError) {
1178
+ failAndClear(endError);
1179
+ return;
1180
+ }
1181
+ // Connect's clean END_STREAM envelope is the protocol terminal. Cursor's RunSSE body can
1182
+ // remain open after this frame (or close through an AbortError), so waiting for HTTP EOF
1183
+ // strands an otherwise completed turn until the outer bridge stall watchdog fires.
1184
+ //
1185
+ // Earlier frames in this serialized frameWork chain have already run. Preserve their real
1186
+ // turnEnded terminal when present; otherwise finalize the clean protocol end once so open
1187
+ // tool calls still fail closed, a text-only turn receives its normal done event, and a
1188
+ // drained client-tool turn does not lose the pending terminal when protocol cleanup clears
1189
+ // its grace timer.
1190
+ const hasPendingClientToolFinalization = this.pendingFinalize !== undefined;
1191
+ if (
1192
+ !this.expectedClose
1193
+ && !state.terminated
1194
+ && !this.emittedTerminal
1195
+ && (
1196
+ state.openToolCalls.size > 0
1197
+ || this.sawAssistantText
1198
+ || hasPendingClientToolFinalization
1199
+ )
1200
+ ) {
1201
+ const terminal = hasPendingClientToolFinalization && state.openToolCalls.size === 0
1202
+ ? finalizeAfterDrain(state)
1203
+ : finalizeTurnEvents(state);
1204
+ for (const event of terminal) push(event);
1205
+ }
1206
+ this.markProtocolComplete();
1207
+ releaseBacklogLease();
1208
+ settler.settleFinish();
858
1209
  return;
859
1210
  }
860
- await this.handleServerMessage(fromBinary(AgentServerMessageSchema, frame.payload), state, push);
1211
+ const decoded = fromBinary(AgentServerMessageSchema, frame.payload);
1212
+ // T04: every decoded frame refreshes the silence clock; only non-liveness frames
1213
+ // refresh the progress clock. First decoded frame arms the watchdog (the first-frame
1214
+ // timer owned everything before this point).
1215
+ const decodedUpdate = decoded.message.case === "interactionUpdate" ? decoded.message.value.message?.case : undefined;
1216
+ const livenessOnly = decodedUpdate === "heartbeat" || decoded.message.case === "conversationCheckpointUpdate";
1217
+ if (!this.streamHealthFail) {
1218
+ const now = Date.now();
1219
+ this.lastInboundFrameAt = now;
1220
+ this.lastMeaningfulFrameAt = now;
1221
+ this.streamHealthFail = failAndClear;
1222
+ }
1223
+ this.noteInboundFrame(livenessOnly);
1224
+ await this.handleServerMessage(decoded, state, push);
861
1225
  };
862
1226
  const drainPendingFrames = () => {
863
1227
  const availableSlots = CURSOR_MAX_PENDING_FRAMES - this.pendingTransportFrames;
864
- if (availableSlots <= 0 || pending.byteLength === 0) {
1228
+ const used = backlogEnd - backlogStart;
1229
+ if (availableSlots <= 0 || used === 0) {
865
1230
  this.updateTransportFlowControl();
866
1231
  return;
867
1232
  }
868
- const previousPayloadBytes = connectBufferedPayloadBytes(pending);
869
- // The decoder materializes payload and residual copies. Keep the aggregate pending owner
870
- // charged until every replacement has been admitted and committed; a failed admission must
871
- // leave that predecessor lease intact for deterministic cleanup.
872
- const decoded = decodeAvailableConnectFrames(
873
- pending,
1233
+ // Cursor decode, zero-copy: frame payloads are views into the backlog, so
1234
+ // the charge TRANSFERS only consumed header bytes leave the counter
1235
+ // (payload bytes stay charged and are released when each frame's work
1236
+ // finishes). An exact 16 MiB payload therefore peaks at 16 MiB + 5, not
1237
+ // at double its size.
1238
+ const decoded = consumeConnectFrames(
1239
+ backlog.subarray(backlogStart, backlogEnd),
874
1240
  CURSOR_MAX_EFFECTIVE_CONNECT_PAYLOAD_BYTES,
875
1241
  availableSlots,
876
- reservePayloadCopy,
877
1242
  );
878
- pending = decoded.remainder;
879
- this.releaseTransportBytes(previousPayloadBytes);
1243
+ if (decoded.frames.length > 0) {
1244
+ backlogStart += decoded.consumedBytes;
1245
+ this.releaseTransportBytes(decoded.consumedBytes - decoded.frames.reduce((n, frame) => n + frame.payload.byteLength, 0));
1246
+ }
880
1247
  for (const frame of decoded.frames) {
881
1248
  this.pendingTransportFrames += 1;
882
1249
  this.updateTransportFlowControl();
@@ -891,43 +1258,42 @@ class LiveCursorTransport implements CursorTransport {
891
1258
  });
892
1259
  }
893
1260
  };
894
- this.stream.on("data", chunk => {
1261
+ const onData = (chunk: string | Uint8Array) => {
895
1262
  this.clearFirstFrameTimer();
1263
+ // Once the turn has settled, late network bytes must never be charged —
1264
+ // the backlog lease is already released and nobody would own these.
1265
+ if (settler.settled()) return;
896
1266
  if (!this.firstFrameLogged) {
897
1267
  this.firstFrameLogged = true;
898
1268
  this.firstFrameAt = Date.now();
899
1269
  debugProviderDiagnostic("cursor", "first-frame", { latencyMs: this.firstFrameAt - this.turnStartedAt });
900
1270
  }
901
1271
  const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
902
- let incomingPayloadBytes = 0;
903
- let incomingCharged = false;
904
- let replacement: ReturnType<typeof reservePayloadCopy> | undefined;
1272
+ let charged = false;
1273
+ let appended = false;
905
1274
  try {
906
- const previousPendingPayloadBytes = connectBufferedPayloadBytes(pending);
907
- const nextPendingPayloadBytes = connectBufferedPayloadBytesAcross(pending, bytes);
908
- incomingPayloadBytes = Math.max(0, nextPendingPayloadBytes - previousPendingPayloadBytes);
909
- this.reserveTransportBytes(incomingPayloadBytes);
910
- incomingCharged = true;
911
- replacement = reservePayloadCopy(nextPendingPayloadBytes);
912
- const nextPending = concatBytes(pending, bytes);
913
- pending = nextPending;
914
- replacement.commitRetained();
915
- replacement = undefined;
916
- this.releaseTransportBytes(previousPendingPayloadBytes + incomingPayloadBytes);
917
- incomingCharged = false;
1275
+ // RAW chunk bytes (headers included) join the backlog charge; consumed
1276
+ // bytes leave it at drain. No whole-backlog replacement copy anymore.
1277
+ this.reserveTransportBytes(bytes.byteLength);
1278
+ charged = true;
1279
+ appendBacklog(bytes);
1280
+ appended = true;
918
1281
  drainPendingFrames();
919
1282
  } catch (err) {
920
- replacement?.release();
921
- if (incomingCharged) this.releaseTransportBytes(incomingPayloadBytes);
1283
+ // Release ONLY when the reservation succeeded but the append never
1284
+ // happened. A failed reservation charged nothing — releasing here
1285
+ // would debit unrelated existing ownership; an appended chunk is owned
1286
+ // by the terminal backlog cleanup.
1287
+ if (charged && !appended) this.releaseTransportBytes(bytes.byteLength);
922
1288
  failAndClear(err instanceof Error ? err : new Error(String(err)));
923
1289
  }
924
- });
925
- this.stream.on("trailers", trailers => {
1290
+ };
1291
+ const onTrailers = (trailers: http2.IncomingHttpHeaders) => {
926
1292
  const status = trailers["grpc-status"];
927
1293
  if (status !== undefined) debugProviderDiagnostic("cursor", "trailers", { grpcStatus: String(status) });
928
1294
  if (status && status !== "0") failAndClear(new Error(`Cursor gRPC error ${status}`));
929
- });
930
- this.stream.on("error", err => {
1295
+ };
1296
+ const onStreamError = (err: unknown) => {
931
1297
  const realErr = err instanceof Error ? err : new Error(String(err));
932
1298
  if (this.expectedClose) {
933
1299
  failAndClear(realErr);
@@ -945,8 +1311,8 @@ class LiveCursorTransport implements CursorTransport {
945
1311
  elapsedMs: Date.now() - this.turnStartedAt,
946
1312
  });
947
1313
  failAndClear(realErr);
948
- });
949
- this.stream.on("end", () => {
1314
+ };
1315
+ const onStreamEnd = () => {
950
1316
  this.clearFirstFrameTimer();
951
1317
  debugProviderDiagnostic("cursor", "stream-end", {
952
1318
  committed: this.committed,
@@ -954,39 +1320,134 @@ class LiveCursorTransport implements CursorTransport {
954
1320
  expectedClose: this.expectedClose,
955
1321
  elapsedMs: Date.now() - this.turnStartedAt,
956
1322
  });
957
- // A zero-frame end without an expected close is an unexpected EOF (peer dropped the
958
- // connection before any response frame) surfacing it as success would silently
959
- // swallow the turn (WP4 review blocker 1). With frames, the protobuf event state
960
- // owns terminal semantics as before.
961
- if (this.framesReceived === 0 && !this.expectedClose) {
962
- settler.settleFail(new Error("Cursor stream ended before any response frame (unexpected EOF)"));
963
- return;
964
- }
965
- settler.settleFinish();
966
- });
1323
+ // Settle only after queued frame work drains to quiescence (the chain can
1324
+ // extend itself while draining), then classify the terminal state:
1325
+ // a trailing incomplete frame is a typed failure, and a zero-frame,
1326
+ // zero-byte end without an expected close stays the existing unexpected
1327
+ // EOF both beat the old silent success.
1328
+ void (async () => {
1329
+ let previous: Promise<void>;
1330
+ do {
1331
+ previous = frameWork;
1332
+ await previous;
1333
+ } while (previous !== frameWork);
1334
+ })().then(() => {
1335
+ if (settler.settled()) return;
1336
+ const leftover = backlogEnd - backlogStart;
1337
+ if (leftover > 0 && !this.expectedClose) {
1338
+ releaseBacklogLease();
1339
+ settler.settleFail(new ConnectFrameError(
1340
+ "frame_incomplete",
1341
+ `Cursor Connect stream ended with ${leftover} unconsumed bytes (incomplete frame)`,
1342
+ ));
1343
+ return;
1344
+ }
1345
+ if (this.framesReceived === 0 && !this.expectedClose) {
1346
+ releaseBacklogLease();
1347
+ settler.settleFail(new Error("Cursor stream ended before any response frame (unexpected EOF)"));
1348
+ return;
1349
+ }
1350
+ // `emittedTerminal` joins dev's two conditions so EOF finalization cannot append a
1351
+ // second terminal after a mapper error already failed the turn (integration 010).
1352
+ if (state.terminated || this.expectedClose || this.emittedTerminal) {
1353
+ releaseBacklogLease();
1354
+ settler.settleFinish();
1355
+ return;
1356
+ }
1357
+ // Open tools fail-closed as a truncation *event* (finalizeTurnEvents), not a thrown
1358
+ // transport error. settleFail here would hide that typed message as adapter_eof.
1359
+ if (state.openToolCalls.size > 0) {
1360
+ for (const event of finalizeTurnEvents(state)) push(event);
1361
+ releaseBacklogLease();
1362
+ settler.settleFinish();
1363
+ return;
1364
+ }
1365
+ if (this.framesReceived > 0 && this.sawAssistantText) {
1366
+ for (const event of finalizeTurnEvents(state)) push(event);
1367
+ releaseBacklogLease();
1368
+ settler.settleFinish();
1369
+ return;
1370
+ }
1371
+ releaseBacklogLease();
1372
+ settler.settleFinish();
1373
+ }).catch((err) => {
1374
+ failAndClear(err instanceof Error ? err : new Error(String(err)));
1375
+ });
1376
+ };
1377
+
1378
+ if (useHttp1) {
1379
+ const providerFetch = this.input.fetch
1380
+ ?? (this.input.provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch;
1381
+ this.http1Connection = new CursorHttp1BidiConnection({
1382
+ baseUrl,
1383
+ token: this.token,
1384
+ clientVersion: CURSOR_CLIENT_VERSION,
1385
+ sessionId: this.sessionId,
1386
+ requestId,
1387
+ translatorBudget: this.translatorBudget,
1388
+ callbacks: {
1389
+ onCommitted: () => {
1390
+ this.committed = true;
1391
+ debugProviderDiagnostic("cursor", "connected", {
1392
+ transport: "http1.1",
1393
+ connectMs: Date.now() - this.turnStartedAt,
1394
+ });
1395
+ },
1396
+ onData,
1397
+ onEnd: onStreamEnd,
1398
+ onError: onStreamError,
1399
+ },
1400
+ ...(providerFetch ? { fetch: providerFetch } : {}),
1401
+ });
1402
+ this.http1Connection.start();
1403
+ } else {
1404
+ session!.on("error", onSessionError);
1405
+ stream!.on("data", onData);
1406
+ stream!.on("trailers", onTrailers);
1407
+ stream!.on("error", onStreamError);
1408
+ stream!.on("end", onStreamEnd);
1409
+ }
967
1410
 
968
1411
  signal?.addEventListener("abort", () => {
969
1412
  this.close();
970
1413
  failAndClear(new Error("Cursor request was aborted"));
971
1414
  }, { once: true });
1415
+ // Close the race between the preflight above and listener installation. No request payload is
1416
+ // written until after this check.
1417
+ if (signal?.aborted) {
1418
+ this.close();
1419
+ failAndClear(signal.reason instanceof Error ? signal.reason : new Error("Cursor request was aborted"));
1420
+ return;
1421
+ }
972
1422
 
973
- this.stream.write(encodeConnectFrame(encodedRequest));
1423
+ this.writeConnectFrame(encodeConnectFrame(encodedRequest));
974
1424
  this.heartbeat = setInterval(() => {
975
- this.stream?.write(encodeClientMessage({
1425
+ this.writeConnectFrame(encodeClientMessage({
976
1426
  message: { case: "clientHeartbeat", value: create(ClientHeartbeatSchema, {}) },
977
1427
  }));
978
1428
  }, HEARTBEAT_MS);
979
1429
  }
980
1430
 
1431
+ capturedConversationCheckpoint(): Uint8Array | undefined {
1432
+ return this.capturedCheckpointBytes;
1433
+ }
1434
+
981
1435
  private async handleServerMessage(
982
1436
  message: AgentServerMessage,
983
1437
  state: ReturnType<typeof createCursorProtobufEventState>,
984
1438
  push: (message: CursorServerMessage) => void,
985
1439
  ): Promise<void> {
986
- if (!this.stream) return;
1440
+ if (!this.stream && !this.http1Connection) return;
987
1441
  debugProviderDiagnostic("cursor", "frame", describeCursorServerFrame(message));
1442
+ if (message.message.case === "conversationCheckpointUpdate") {
1443
+ try {
1444
+ this.capturedCheckpointBytes = toBinary(ConversationStateStructureSchema, message.message.value);
1445
+ } catch {
1446
+ this.capturedCheckpointBytes = undefined;
1447
+ }
1448
+ }
988
1449
  if (message.message.case === "kvServerMessage") {
989
- this.stream.write(encodeConnectFrame(handleCursorNativeKv(message.message.value, this.blobRequestScope)));
1450
+ this.writeConnectFrame(encodeConnectFrame(handleCursorNativeKv(message.message.value, this.blobRequestScope)));
990
1451
  return;
991
1452
  }
992
1453
  if (message.message.case === "execServerMessage") {
@@ -1007,7 +1468,7 @@ class LiveCursorTransport implements CursorTransport {
1007
1468
  // run the same local action twice.
1008
1469
  push({ type: "local_side_effect" });
1009
1470
  const replies = await handleCursorNativeExec(message.message.value, this.execContext);
1010
- for (const reply of replies) this.stream.write(encodeConnectFrame(reply));
1471
+ for (const reply of replies) this.writeConnectFrame(encodeConnectFrame(reply));
1011
1472
  return;
1012
1473
  }
1013
1474
  if (message.message.case === "interactionQuery") {
@@ -1017,29 +1478,58 @@ class LiveCursorTransport implements CursorTransport {
1017
1478
  const query = message.message.value;
1018
1479
  const plan = planInteractionQueryReply(query);
1019
1480
  debugProviderDiagnostic("cursor", "interaction-query", { id: query.id, queryCase: query.query.case ?? "unknown", reply: plan.replyCase });
1020
- this.stream.write(encodeClientMessage({ message: { case: "interactionResponse", value: plan.response } }));
1481
+ this.writeConnectFrame(encodeClientMessage({ message: { case: "interactionResponse", value: plan.response } }));
1021
1482
  if (!state.terminated) {
1022
- if (plan.planText) push({ type: "text", text: plan.planText });
1483
+ if (plan.planText) {
1484
+ this.sawAssistantText = true;
1485
+ push({ type: "text", text: plan.planText });
1486
+ }
1023
1487
  push({ type: "heartbeat" });
1024
1488
  }
1025
1489
  return;
1026
1490
  }
1491
+ // A completion may carry only callId. Capture its ownership before mapping removes the open
1492
+ // call, because the embedded-tool classifier cannot identify that valid compact frame.
1493
+ const update = message.message.case === "interactionUpdate" ? message.message.value.message : undefined;
1494
+ if (update?.case === "turnEnded") {
1495
+ // T03: the application turn is complete. Close our side of HTTP/2 after a short
1496
+ // grace so a held-open server response cannot pin the turn to the bridge's idle
1497
+ // timeout (senpi #1062). finalizeTurnEvents already emitted done via the mapper.
1498
+ this.closeAfterTurnEnded();
1499
+ }
1500
+ const completesOpenClientTool = update?.case === "toolCallCompleted"
1501
+ && state.openToolCalls.has(update.value.callId);
1502
+ const awaitedNativeArgsBeforeMapping = update?.case === "toolCallCompleted"
1503
+ && state.openToolCalls.get(update.value.callId)?.awaitingNativeArgs === true;
1027
1504
  const mapped = mapCursorProtobufServerMessage(message, state);
1505
+ if (mapped.some(event => event.type === "text")) this.sawAssistantText = true;
1506
+ const beganAwaitingNativeClientToolArgs = update?.case === "toolCallCompleted"
1507
+ && !awaitedNativeArgsBeforeMapping
1508
+ && state.openToolCalls.get(update.value.callId)?.awaitingNativeArgs === true;
1028
1509
  if (mapped.length > 0) {
1029
1510
  // A client tool call announced/committed via interactionUpdate (toolCallStarted/partialToolCall/
1030
1511
  // toolCallCompleted) changes the call set, so revoke any finalize armed by an earlier drain.
1031
- if (isClientToolFrame(message)) this.noteClientToolActivity();
1512
+ // A completion can also commit and drain a late call without a following mcpArgs frame; in
1513
+ // that case re-arm finalization here so the Responses bridge does not wait forever.
1514
+ const clientToolFrame = completesOpenClientTool || isClientToolFrame(message);
1515
+ if (clientToolFrame) this.noteClientToolActivity();
1032
1516
  for (const event of mapped) push(event);
1517
+ if (
1518
+ clientToolFrame
1519
+ && state.openToolCalls.size === 0
1520
+ && mapped.some(event => event.type === "tool_call_end")
1521
+ ) this.scheduleClientToolFinalize(state, push);
1033
1522
  return;
1034
1523
  }
1035
1524
  // The frame produced no outward Responses event (e.g. toolCallStarted / partialToolCall args
1036
- // buffering, toolCallDelta, tokenDelta, or a checkpoint update). Tool-call protocol events are
1037
- // deferred to completion for atomic, parallel-safe emission, so a turn that silently assembles
1038
- // several tool calls can otherwise exceed the bridge's stall watchdog (upstream_stall_timeout).
1525
+ // buffering, a completion waiting for native args, toolCallDelta, tokenDelta, or a checkpoint
1526
+ // update). Tool-call protocol events are deferred to completion for atomic, parallel-safe
1527
+ // emission, so a turn that silently assembles several tool calls can otherwise exceed the
1528
+ // bridge's stall watchdog (upstream_stall_timeout).
1039
1529
  // Emit a liveness heartbeat for these progress frames so the watchdog sees the upstream is alive.
1040
1530
  // Never after a terminal (done/truncation): a stray post-terminal frame must stay fully inert.
1041
- if (!state.terminated && isCursorProgressFrame(message)) {
1042
- if (isClientToolFrame(message)) this.noteClientToolActivity();
1531
+ if (!state.terminated && (isCursorProgressFrame(message) || beganAwaitingNativeClientToolArgs)) {
1532
+ if (isClientToolFrame(message) || beganAwaitingNativeClientToolArgs) this.noteClientToolActivity();
1043
1533
  push({ type: "heartbeat" });
1044
1534
  }
1045
1535
  }
@@ -1140,47 +1630,6 @@ export function isClientToolFrame(message: AgentServerMessage): boolean {
1140
1630
  }
1141
1631
  }
1142
1632
 
1143
- function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
1144
- const out = new Uint8Array(a.length + b.length);
1145
- out.set(a);
1146
- out.set(b, a.length);
1147
- return out;
1148
- }
1149
-
1150
- function connectBufferedPayloadBytes(input: Uint8Array): number {
1151
- let offset = 0;
1152
- let payloadBytes = 0;
1153
- while (input.byteLength - offset >= 5) {
1154
- const length = new DataView(input.buffer, input.byteOffset + offset, input.byteLength - offset).getUint32(1, false);
1155
- const available = Math.min(length, input.byteLength - offset - 5);
1156
- payloadBytes += available;
1157
- if (available < length) break;
1158
- offset += 5 + length;
1159
- }
1160
- return payloadBytes;
1161
- }
1162
-
1163
- /** Payload-byte count for a virtual concatenation, without allocating the concatenated buffer. */
1164
- function connectBufferedPayloadBytesAcross(first: Uint8Array, second: Uint8Array): number {
1165
- const totalBytes = first.byteLength + second.byteLength;
1166
- const byteAt = (index: number): number => index < first.byteLength
1167
- ? first[index]!
1168
- : second[index - first.byteLength]!;
1169
- let offset = 0;
1170
- let payloadBytes = 0;
1171
- while (totalBytes - offset >= 5) {
1172
- const length = (byteAt(offset + 1) * 0x1000000)
1173
- + (byteAt(offset + 2) << 16)
1174
- + (byteAt(offset + 3) << 8)
1175
- + byteAt(offset + 4);
1176
- const available = Math.min(length, totalBytes - offset - 5);
1177
- payloadBytes += available;
1178
- if (available < length) break;
1179
- offset += 5 + length;
1180
- }
1181
- return payloadBytes;
1182
- }
1183
-
1184
1633
  /** Host-only label for Cursor transport diagnostics — never leaks path/query/credentials. */
1185
1634
  function cursorHostLabel(baseUrl: string): string {
1186
1635
  try {
@@ -1212,3 +1661,7 @@ function cursorConnectErrorCode(payload: Uint8Array): string | undefined {
1212
1661
  export function createLiveCursorTransport(input: CursorTransportFactoryInput): CursorTransport {
1213
1662
  return new LiveCursorTransport(input);
1214
1663
  }
1664
+
1665
+ export function capturedCursorCheckpointBytes(transport: CursorTransport): Uint8Array | undefined {
1666
+ return transport.capturedConversationCheckpoint?.();
1667
+ }