@iislee/opencodex 2.11.0 → 2.31.0

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