@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922

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 (374) hide show
  1. package/AGENTS_INSTALL.md +64 -0
  2. package/README.md +28 -1
  3. package/bin/ocx.mjs +382 -209
  4. package/gui/dist/assets/App-E64Rzjap.js +50 -0
  5. package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
  6. package/gui/dist/assets/index-DpdfZWMK.js +86 -0
  7. package/gui/dist/assets/index-_bpvxJu0.css +1 -0
  8. package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
  9. package/gui/dist/favicon.png +0 -0
  10. package/gui/dist/index.html +2 -2
  11. package/gui/dist/provider-icons/stepfun-color.svg +1 -0
  12. package/package.json +8 -1
  13. package/src/adapters/anthropic-image-codec.ts +16 -2
  14. package/src/adapters/anthropic-image-normalize.ts +49 -2
  15. package/src/adapters/anthropic.ts +20 -1
  16. package/src/adapters/base.ts +23 -0
  17. package/src/adapters/coding-agent/protocol.ts +36 -6
  18. package/src/adapters/coding-agent/turn.ts +32 -4
  19. package/src/adapters/command-code.ts +52 -4
  20. package/src/adapters/cursor/catalog.ts +51 -7
  21. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  22. package/src/adapters/cursor/discovery.ts +11 -8
  23. package/src/adapters/cursor/live-transport.ts +26 -9
  24. package/src/adapters/cursor/protobuf-request.ts +6 -3
  25. package/src/adapters/cursor/request-builder.ts +20 -4
  26. package/src/adapters/cursor/transport.ts +19 -0
  27. package/src/adapters/cursor.ts +14 -3
  28. package/src/adapters/declaration-carrier.ts +45 -0
  29. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  30. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  31. package/src/adapters/devin.ts +125 -36
  32. package/src/adapters/google-antigravity-replay.ts +1 -1
  33. package/src/adapters/google-antigravity-wire.ts +34 -7
  34. package/src/adapters/google-errors.ts +7 -1
  35. package/src/adapters/google-http.ts +49 -10
  36. package/src/adapters/google-tool-schema.ts +595 -31
  37. package/src/adapters/google-wire-compiler.ts +93 -10
  38. package/src/adapters/google-wire-shape.ts +461 -0
  39. package/src/adapters/google.ts +66 -11
  40. package/src/adapters/image.ts +4 -1
  41. package/src/adapters/input-media-guard.ts +21 -9
  42. package/src/adapters/kiro/usage.ts +3 -2
  43. package/src/adapters/kiro-tool-fallback.ts +1 -1
  44. package/src/adapters/ollama-native.ts +6 -0
  45. package/src/adapters/openai-chat/developer-role.ts +61 -0
  46. package/src/adapters/openai-chat/messages.ts +46 -27
  47. package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
  48. package/src/adapters/openai-chat/passthrough.ts +33 -9
  49. package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
  50. package/src/adapters/openai-chat-images.ts +3 -1
  51. package/src/adapters/openai-chat.ts +23 -58
  52. package/src/adapters/openai-responses/image-gen.ts +8 -6
  53. package/src/adapters/openai-responses/passthrough.ts +17 -3
  54. package/src/adapters/openai-responses/reasoning.ts +7 -0
  55. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  56. package/src/adapters/registry.ts +3 -2
  57. package/src/adapters/run-turn-queue.ts +178 -29
  58. package/src/adapters/xai-web-search.ts +16 -1
  59. package/src/bridge/errors.ts +8 -2
  60. package/src/bridge/response-json.ts +9 -1
  61. package/src/bridge/sse.ts +13 -151
  62. package/src/chat/inbound.ts +141 -5
  63. package/src/claude/desktop-3p.ts +7 -1
  64. package/src/claude/desktop-first-party.ts +183 -0
  65. package/src/claude/desktop-gateway-state.ts +41 -0
  66. package/src/claude/inbound-content-options.ts +6 -0
  67. package/src/claude/inbound.ts +32 -6
  68. package/src/claude/intercept/connect-proxy.ts +179 -0
  69. package/src/claude/intercept/listener.ts +122 -0
  70. package/src/claude/intercept/local-ca.ts +298 -0
  71. package/src/claude/intercept/runtime.ts +98 -0
  72. package/src/claude/intercept/settings.ts +189 -0
  73. package/src/cli/access.ts +87 -0
  74. package/src/cli/account-auth.ts +19 -0
  75. package/src/cli/account-extended.ts +4 -4
  76. package/src/cli/capabilities.ts +31 -0
  77. package/src/cli/claude-desktop.ts +206 -16
  78. package/src/cli/codex-shim-autorestore.ts +3 -0
  79. package/src/cli/companion.ts +56 -0
  80. package/src/cli/dispatch.ts +46 -7
  81. package/src/cli/doctor.ts +28 -9
  82. package/src/cli/ensure-desired-integrations.ts +43 -5
  83. package/src/cli/help.ts +7 -9
  84. package/src/cli/hub.ts +3 -2
  85. package/src/cli/index.ts +203 -59
  86. package/src/cli/init.ts +8 -0
  87. package/src/cli/integrations.ts +7 -1
  88. package/src/cli/opencode.ts +2 -2
  89. package/src/cli/provider.ts +13 -1
  90. package/src/cli/registry.ts +41 -2
  91. package/src/cli/resolve.ts +230 -0
  92. package/src/cli/root.ts +24 -1
  93. package/src/cli/start-ownership-publication.ts +56 -0
  94. package/src/cli/status-probes.ts +2 -18
  95. package/src/cli/status.ts +62 -0
  96. package/src/cli/stop-report.ts +143 -0
  97. package/src/cli/uninstall-plan.ts +9 -0
  98. package/src/client/machine-api.ts +2 -2
  99. package/src/client/machine-listener.ts +4 -7
  100. package/src/client/runtime.ts +26 -2
  101. package/src/clients/aside-profiles.ts +4 -0
  102. package/src/clients/config-export/zcode-store.ts +157 -0
  103. package/src/clients/config-export.ts +36 -0
  104. package/src/codex/account-store.ts +65 -0
  105. package/src/codex/app-server-processes.ts +72 -40
  106. package/src/codex/auth-api/account-list.ts +19 -11
  107. package/src/codex/auth-api/login-flow.ts +6 -1
  108. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  109. package/src/codex/autostart-health.ts +28 -0
  110. package/src/codex/catalog/build-entries.ts +2 -2
  111. package/src/codex/catalog/effort.ts +3 -3
  112. package/src/codex/catalog/gather-capture.ts +21 -2
  113. package/src/codex/catalog/model-hints.ts +29 -28
  114. package/src/codex/catalog/parsing.ts +7 -0
  115. package/src/codex/catalog/provider-models.ts +43 -17
  116. package/src/codex/catalog/retained-sync.ts +24 -28
  117. package/src/codex/catalog/routed-gather.ts +19 -0
  118. package/src/codex/context-compat.ts +5 -2
  119. package/src/codex/convergence.ts +2 -2
  120. package/src/codex/desired-state.ts +4 -1
  121. package/src/codex/history-job.ts +6 -6
  122. package/src/codex/history-provider.ts +31 -166
  123. package/src/codex/history-rollout-read.ts +174 -0
  124. package/src/codex/inject/config-toml.ts +41 -6
  125. package/src/codex/inject/paginated-openai-compat.ts +90 -0
  126. package/src/codex/inject.ts +18 -15
  127. package/src/codex/injected-marker.ts +18 -0
  128. package/src/codex/internal/catalog-writer.ts +33 -1
  129. package/src/codex/main-account.ts +6 -0
  130. package/src/codex/model-cache.ts +99 -6
  131. package/src/codex/model-entitlement-admission.ts +59 -0
  132. package/src/codex/model-entitlements.ts +116 -54
  133. package/src/codex/native-main-admission.ts +83 -0
  134. package/src/codex/observed-model-denials.ts +101 -8
  135. package/src/codex/prompt-text-probe.ts +9 -6
  136. package/src/codex/routing/health-store.ts +39 -0
  137. package/src/codex/routing/selection.ts +37 -1
  138. package/src/codex/routing.ts +12 -42
  139. package/src/codex/shim-templates.ts +29 -3
  140. package/src/codex/shim.ts +1 -1
  141. package/src/codex/subagent-model-fallback.ts +22 -4
  142. package/src/combos/failover.ts +3 -0
  143. package/src/companion/settings.ts +132 -0
  144. package/src/config/admitted-identity.ts +222 -0
  145. package/src/config/atomic-write.ts +117 -5
  146. package/src/config/diagnostics.ts +22 -1
  147. package/src/config/feature-flags.ts +5 -0
  148. package/src/config/load-degrade.ts +52 -7
  149. package/src/config/process-state.ts +1 -1
  150. package/src/config/proxy-env.ts +8 -2
  151. package/src/config/schema/compaction-triggers.ts +11 -0
  152. package/src/config/schema/config-schema.ts +31 -1
  153. package/src/config/schema/leaf-validators.ts +59 -0
  154. package/src/config.ts +3 -3
  155. package/src/generated/compatibility-version.json +604 -280
  156. package/src/grok/reset-coupons.ts +38 -19
  157. package/src/images/loop.ts +6 -1
  158. package/src/integrations/aside-profile-context.ts +37 -3
  159. package/src/integrations/aside-profile-journal.ts +68 -3
  160. package/src/integrations/aside-profiles.ts +128 -3
  161. package/src/integrations/config-io.ts +44 -10
  162. package/src/integrations/merge.ts +120 -13
  163. package/src/integrations/mutation-plan.ts +921 -0
  164. package/src/integrations/registry.ts +38 -0
  165. package/src/integrations/state.ts +78 -45
  166. package/src/integrations/target.ts +208 -0
  167. package/src/integrations/writer.ts +134 -110
  168. package/src/lab/conformance/fixture-provider.ts +5 -0
  169. package/src/lab/live/transport.ts +4 -0
  170. package/src/lab/live/types.ts +5 -0
  171. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  172. package/src/lib/admin-secrets.ts +9 -1
  173. package/src/lib/browser-launch-notice.ts +59 -0
  174. package/src/lib/bun-runtime.ts +6 -2
  175. package/src/lib/debug-log-buffer.ts +6 -1
  176. package/src/lib/debug.ts +63 -0
  177. package/src/lib/errors.ts +79 -0
  178. package/src/lib/http-response-semantics.ts +57 -0
  179. package/src/lib/lab-live-pinned-sender.ts +26 -12
  180. package/src/lib/open-url.ts +51 -7
  181. package/src/lib/package-tree-integrity.ts +2 -1
  182. package/src/lib/package-version.ts +8 -0
  183. package/src/lib/pinned-http.ts +142 -2
  184. package/src/lib/plain-data.ts +103 -0
  185. package/src/lib/process-control.ts +13 -5
  186. package/src/lib/provider-egress.ts +310 -0
  187. package/src/lib/provider-outbound.ts +109 -16
  188. package/src/lib/proxy-env.ts +82 -7
  189. package/src/lib/request-execution-budget.ts +72 -0
  190. package/src/lib/request-failure-attribution.ts +183 -0
  191. package/src/lib/request-failure-model.ts +236 -0
  192. package/src/lib/request-resend-gate.ts +138 -0
  193. package/src/lib/socks5-fetch.ts +136 -26
  194. package/src/lib/spend-ledger-owner.ts +364 -0
  195. package/src/lib/spend-reservation-ledger.ts +218 -27
  196. package/src/lib/standalone.ts +16 -0
  197. package/src/lib/upstream-retry.ts +167 -16
  198. package/src/lib/windows-system-proxy.ts +16 -11
  199. package/src/lib/winsw.ts +2 -2
  200. package/src/oauth/callback-server.ts +4 -3
  201. package/src/oauth/generic-account-failover.ts +1 -0
  202. package/src/oauth/health.ts +12 -1
  203. package/src/oauth/index.ts +27 -108
  204. package/src/oauth/login-cli.ts +80 -29
  205. package/src/oauth/login-flow-state.ts +127 -0
  206. package/src/providers/api-key-resolve.ts +133 -0
  207. package/src/providers/api-key-selection.ts +5 -1
  208. package/src/providers/derive.ts +34 -17
  209. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  210. package/src/providers/key-failover.ts +97 -19
  211. package/src/providers/key-store.ts +34 -110
  212. package/src/providers/model-rename-fields.ts +147 -0
  213. package/src/providers/model-rename-migration.ts +179 -38
  214. package/src/providers/model-rename-startup.ts +7 -5
  215. package/src/providers/openai-virtual-models.ts +42 -2
  216. package/src/providers/quota/antigravity.ts +22 -2
  217. package/src/providers/quota/vendor-probes-key.ts +38 -23
  218. package/src/providers/reasoning-metadata.ts +43 -18
  219. package/src/providers/registry/entries-core.ts +47 -20
  220. package/src/providers/registry/entries-extended.ts +63 -4
  221. package/src/providers/registry/model-ids.ts +168 -0
  222. package/src/providers/registry/model-seeds.ts +56 -10
  223. package/src/providers/registry/types.ts +2 -0
  224. package/src/providers/resolved-model-policy-merge.ts +167 -0
  225. package/src/providers/resolved-model-policy.ts +406 -0
  226. package/src/providers/stale-vision-classification-migration.ts +137 -0
  227. package/src/providers/xai-transport.ts +12 -1
  228. package/src/reasoning-effort.ts +8 -0
  229. package/src/responses/apply-patch-envelope.ts +0 -12
  230. package/src/responses/freeform-wrapper-scan.ts +279 -0
  231. package/src/responses/function-call-compat.ts +38 -1
  232. package/src/responses/inline-document.ts +65 -0
  233. package/src/responses/input-media.ts +42 -8
  234. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  235. package/src/responses/muse-tool-name-alias.ts +19 -0
  236. package/src/responses/parser-content.ts +8 -2
  237. package/src/responses/parser-tools.ts +3 -0
  238. package/src/responses/parser.ts +3 -1
  239. package/src/responses/progressive-freeform-input.ts +130 -0
  240. package/src/responses/reasoning-envelope.ts +30 -0
  241. package/src/responses/schema.ts +3 -0
  242. package/src/responses/state.ts +5 -12
  243. package/src/responses/tool-name-aliases.ts +15 -1
  244. package/src/router.ts +108 -117
  245. package/src/routing/compatibility/behavior.ts +9 -0
  246. package/src/routing/compatibility/subject.ts +16 -1
  247. package/src/server/adapter-resolve.ts +9 -0
  248. package/src/server/admission-model-scope.ts +219 -0
  249. package/src/server/audio-live.ts +9 -3
  250. package/src/server/audio-upstream.ts +18 -0
  251. package/src/server/auth-cors.ts +29 -0
  252. package/src/server/chat-completions.ts +60 -4
  253. package/src/server/chat-native.ts +19 -4
  254. package/src/server/claude-messages.ts +61 -20
  255. package/src/server/effort-row.ts +11 -3
  256. package/src/server/grok-responses-control-frame.ts +160 -1
  257. package/src/server/grok-responses-snapshot-repair.ts +113 -11
  258. package/src/server/gui-freshness.ts +103 -0
  259. package/src/server/gui-static.ts +7 -9
  260. package/src/server/images.ts +59 -6
  261. package/src/server/index/claude-intercept-lifecycle.ts +49 -0
  262. package/src/server/index/serve-options.ts +142 -39
  263. package/src/server/index/spend-ledger-lifecycle.ts +92 -0
  264. package/src/server/index/startup-warnings.ts +24 -0
  265. package/src/server/index/websocket-handler.ts +6 -1
  266. package/src/server/index.ts +34 -38
  267. package/src/server/lifecycle.ts +4 -4
  268. package/src/server/live-call-bindings.ts +6 -0
  269. package/src/server/live.ts +88 -3
  270. package/src/server/management/agent-settings-routes.ts +121 -36
  271. package/src/server/management/aside-profile-routes.ts +266 -7
  272. package/src/server/management/companion-routes.ts +77 -0
  273. package/src/server/management/config-routes.ts +18 -2
  274. package/src/server/management/context.ts +3 -0
  275. package/src/server/management/integration-routes.ts +287 -5
  276. package/src/server/management/logs-usage-routes.ts +19 -0
  277. package/src/server/management/metrics-routes.ts +20 -0
  278. package/src/server/management/model-rows.ts +224 -12
  279. package/src/server/management/native-integration-routes.ts +103 -6
  280. package/src/server/management/oauth-account-routes.ts +45 -7
  281. package/src/server/management/route-registry.ts +20 -0
  282. package/src/server/management/shared.ts +28 -4
  283. package/src/server/management/system-restart.ts +7 -2
  284. package/src/server/management/system-routes.ts +2 -0
  285. package/src/server/management/usage-aggregate-cache.ts +4 -0
  286. package/src/server/management/usage-timeline-routes.ts +44 -0
  287. package/src/server/management-api.ts +10 -9
  288. package/src/server/management-auth.ts +15 -1
  289. package/src/server/proxy-liveness.ts +75 -0
  290. package/src/server/readiness.ts +29 -10
  291. package/src/server/relay-eager.ts +24 -2
  292. package/src/server/relay.ts +138 -12
  293. package/src/server/request-log-failure-attribution.ts +99 -0
  294. package/src/server/request-log.ts +169 -2
  295. package/src/server/request-metrics.ts +298 -0
  296. package/src/server/responses/adapter-continuation.ts +3 -3
  297. package/src/server/responses/adapter-dispatch.ts +11 -6
  298. package/src/server/responses/codex-ws-wire.ts +34 -8
  299. package/src/server/responses/combo-stream-preflight.ts +168 -6
  300. package/src/server/responses/compact.ts +43 -10
  301. package/src/server/responses/compaction-routing.ts +111 -0
  302. package/src/server/responses/core-codex-account.ts +8 -3
  303. package/src/server/responses/core-combo.ts +7 -7
  304. package/src/server/responses/core-normalize.ts +6 -12
  305. package/src/server/responses/core-opaque-recovery.ts +91 -0
  306. package/src/server/responses/core-options.ts +4 -0
  307. package/src/server/responses/encrypted-payload.ts +20 -2
  308. package/src/server/responses/fetch-helpers.ts +124 -8
  309. package/src/server/responses/input-admission.ts +10 -0
  310. package/src/server/responses/passthrough-delivery.ts +45 -15
  311. package/src/server/responses/passthrough-dispatch.ts +215 -44
  312. package/src/server/responses/passthrough-error.ts +27 -8
  313. package/src/server/responses/policy-fallback.ts +5 -13
  314. package/src/server/responses/request-prepare.ts +109 -18
  315. package/src/server/responses/request-send-budget.ts +17 -1
  316. package/src/server/responses/request-sidecar-auth.ts +1 -1
  317. package/src/server/responses/request-transport.ts +26 -6
  318. package/src/server/responses/reset-replay.ts +108 -0
  319. package/src/server/responses/run-turn-execution.ts +25 -3
  320. package/src/server/responses/sidecar-execution.ts +17 -2
  321. package/src/server/responses/ws-upstream.ts +14 -27
  322. package/src/server/responses-custom-tool-repair.ts +27 -54
  323. package/src/server/responses-request-tool-scope.ts +214 -0
  324. package/src/server/responses-undeclared-tool-guard.ts +35 -2
  325. package/src/server/search.ts +25 -1
  326. package/src/server/sse-payload-rewrite.ts +1 -1
  327. package/src/server/usage-ledger-retention.ts +73 -0
  328. package/src/service/cli.ts +48 -2
  329. package/src/service/health.ts +3 -2
  330. package/src/service/install-state-contract.d.mts +27 -0
  331. package/src/service/install-state-contract.mjs +34 -0
  332. package/src/service/launchd.ts +1 -1
  333. package/src/service/orchestration.ts +2 -4
  334. package/src/service/ownership-compatibility.ts +164 -0
  335. package/src/service/ownership-mutation-lease.d.mts +32 -0
  336. package/src/service/ownership-mutation-lease.mjs +211 -0
  337. package/src/service/repair.ts +45 -1
  338. package/src/service/state-lock.ts +269 -0
  339. package/src/service/state-record.d.mts +36 -0
  340. package/src/service/state-record.mjs +138 -0
  341. package/src/service/state.ts +582 -68
  342. package/src/service/windows-taskxml.ts +11 -10
  343. package/src/service.ts +7 -3
  344. package/src/tray/windows-tray.ps1 +156 -4
  345. package/src/types/config.ts +48 -3
  346. package/src/types/provider.ts +91 -0
  347. package/src/types/request.ts +30 -2
  348. package/src/types/tools.ts +33 -0
  349. package/src/types.ts +4 -0
  350. package/src/update/index.ts +207 -63
  351. package/src/update/job.ts +9 -5
  352. package/src/update/ownership-transaction.ts +47 -0
  353. package/src/update/restart-ownership.ts +54 -0
  354. package/src/update/runtime-ownership.d.mts +40 -0
  355. package/src/update/runtime-ownership.mjs +122 -0
  356. package/src/usage/attempt-delivery.ts +198 -0
  357. package/src/usage/cache-diagnostic.ts +305 -0
  358. package/src/usage/failure-fingerprint.ts +118 -0
  359. package/src/usage/failure-projection-cache.ts +174 -0
  360. package/src/usage/failure-projection.ts +174 -0
  361. package/src/usage/ledger-retention.ts +165 -0
  362. package/src/usage/log.ts +126 -79
  363. package/src/usage/request-outcome.ts +150 -0
  364. package/src/usage/retention-contract.ts +28 -0
  365. package/src/usage/summary.ts +2 -2
  366. package/src/usage/telemetry-contract.ts +237 -0
  367. package/src/usage/timeline.ts +236 -0
  368. package/src/vision/eligibility.ts +88 -9
  369. package/src/vision/plan.ts +34 -10
  370. package/src/web-search/alpha-search.ts +21 -1
  371. package/src/web-search/executor.ts +41 -2
  372. package/src/web-search/loop.ts +6 -1
  373. package/gui/dist/assets/index-C5IebErG.js +0 -136
  374. package/gui/dist/assets/index-OESInAjC.css +0 -1
@@ -3,6 +3,7 @@ import {
3
3
  normalizeCursorClaudeId,
4
4
  type NormalizedCursorClaudeId,
5
5
  } from "./claude-id";
6
+ import { createHash } from "node:crypto";
6
7
 
7
8
  /**
8
9
  * Cursor umbrella catalog — the single source of truth for cursor model
@@ -599,7 +600,7 @@ export function resolveCursorSelection(
599
600
  pickedId: string,
600
601
  reasoning: string | undefined,
601
602
  liveMaxModeIds?: ReadonlySet<string>,
602
- options: { fast?: boolean } = {},
603
+ options: { fast?: boolean; liveRosterScope?: string } = {},
603
604
  ): CursorResolvedSelection {
604
605
  const parsed = parseCursorVariantId(pickedId);
605
606
  if (!parsed.known) {
@@ -617,7 +618,10 @@ export function resolveCursorSelection(
617
618
  const requested = parsed.level ?? reasoning;
618
619
  const effort = cursorVariantEffort(spec, requested);
619
620
  const requestedClaude = normalizeCursorClaudeId(pickedId);
620
- const claudeIdentity = liveCursorClaudeWireIdentities.get(parsed.baseId)
621
+ const scopedClaudeIdentities = options.liveRosterScope
622
+ ? liveCursorClaudeWireIdentitiesByScope.get(options.liveRosterScope)
623
+ : liveCursorClaudeWireIdentities;
624
+ const claudeIdentity = scopedClaudeIdentities?.get(parsed.baseId)
621
625
  ?? (requestedClaude
622
626
  ? { sourceBaseId: requestedClaude.sourceBaseId, spelling: requestedClaude.spelling }
623
627
  : undefined);
@@ -626,7 +630,9 @@ export function resolveCursorSelection(
626
630
  ? `${capability.wirePrefix}${canonicalId}`
627
631
  : canonicalId;
628
632
  const ultraRequested = parsed.ultra || reasoning?.toLowerCase() === "ultra";
629
- const evidence = liveMaxModeIds ?? liveCursorMaxModeBases;
633
+ const evidence = liveMaxModeIds
634
+ ?? (options.liveRosterScope ? liveCursorMaxModeBasesByScope.get(options.liveRosterScope) : undefined)
635
+ ?? liveCursorMaxModeBases;
630
636
  const maxModeArmed = capability.maxModeVerified === true || evidence.has(parsed.baseId);
631
637
  return { wireId, canonicalId, maxMode: ultraRequested && maxModeArmed, known: true };
632
638
  }
@@ -639,15 +645,36 @@ export function resolveCursorSelection(
639
645
  */
640
646
  let liveCursorMaxModeBases: ReadonlySet<string> = new Set();
641
647
  let liveCursorClaudeWireIdentities: ReadonlyMap<string, CursorLiveClaudeWireIdentity> = new Map();
648
+ const liveCursorMaxModeBasesByScope = new Map<string, ReadonlySet<string>>();
649
+ const liveCursorClaudeWireIdentitiesByScope = new Map<string, ReadonlyMap<string, CursorLiveClaudeWireIdentity>>();
650
+ const liveCursorRosterScopesByProvider = new Map<string, Set<string>>();
651
+
652
+ /** Non-secret key binding live roster evidence to one upstream destination and credential. */
653
+ export function cursorLiveRosterScope(baseUrl: string | undefined, credential: string): string {
654
+ const destination = (baseUrl?.trim().replace(/\/+$/, "") || "https://api2.cursor.sh");
655
+ return createHash("sha256")
656
+ .update("ocx:cursor:live-roster\0")
657
+ .update(destination)
658
+ .update("\0")
659
+ .update(credential)
660
+ .digest("hex");
661
+ }
642
662
 
643
- export function recordLiveCursorClaudeModels(liveIds: readonly string[]): void {
663
+ export function recordLiveCursorClaudeModels(liveIds: readonly string[], scope?: { provider: string; key: string }): void {
644
664
  const next = new Map<string, CursorLiveClaudeWireIdentity>();
645
665
  for (const rawId of liveIds) {
646
666
  const n = normalizeCursorClaudeId(rawId.startsWith("cursor-") ? rawId.slice(7) : rawId);
647
667
  if (!n || !CURSOR_CAPABILITIES[n.canonicalBaseId]) continue;
648
668
  if (!next.has(n.canonicalBaseId)) next.set(n.canonicalBaseId, { sourceBaseId: n.sourceBaseId, spelling: n.spelling });
649
669
  }
650
- liveCursorClaudeWireIdentities = next;
670
+ if (scope) {
671
+ liveCursorClaudeWireIdentitiesByScope.set(scope.key, next);
672
+ const scopes = liveCursorRosterScopesByProvider.get(scope.provider) ?? new Set<string>();
673
+ scopes.add(scope.key);
674
+ liveCursorRosterScopesByProvider.set(scope.provider, scopes);
675
+ } else {
676
+ liveCursorClaudeWireIdentities = next;
677
+ }
651
678
  }
652
679
 
653
680
  export function liveCursorClaudeWireIdentitiesForTests(): ReadonlyMap<string, CursorLiveClaudeWireIdentity> {
@@ -656,15 +683,32 @@ export function liveCursorClaudeWireIdentitiesForTests(): ReadonlyMap<string, Cu
656
683
 
657
684
  export function resetLiveCursorClaudeWireIdentitiesForTests(): void {
658
685
  liveCursorClaudeWireIdentities = new Map();
686
+ liveCursorClaudeWireIdentitiesByScope.clear();
687
+ liveCursorRosterScopesByProvider.clear();
659
688
  }
660
689
 
661
- export function recordLiveCursorMaxModeModels(liveIds: readonly string[]): void {
690
+ export function recordLiveCursorMaxModeModels(liveIds: readonly string[], scope?: { provider: string; key: string }): void {
662
691
  const bases = new Set<string>();
663
692
  for (const id of liveIds) {
664
693
  const parsed = parseCursorVariantId(id);
665
694
  if (parsed.known) bases.add(parsed.baseId);
666
695
  }
667
- liveCursorMaxModeBases = bases;
696
+ if (scope) liveCursorMaxModeBasesByScope.set(scope.key, bases);
697
+ else liveCursorMaxModeBases = bases;
698
+ }
699
+
700
+ export function clearLiveCursorRosterState(provider?: string): void {
701
+ if (!provider) {
702
+ liveCursorClaudeWireIdentitiesByScope.clear();
703
+ liveCursorMaxModeBasesByScope.clear();
704
+ liveCursorRosterScopesByProvider.clear();
705
+ return;
706
+ }
707
+ for (const scope of liveCursorRosterScopesByProvider.get(provider) ?? []) {
708
+ liveCursorClaudeWireIdentitiesByScope.delete(scope);
709
+ liveCursorMaxModeBasesByScope.delete(scope);
710
+ }
711
+ liveCursorRosterScopesByProvider.delete(provider);
668
712
  }
669
713
 
670
714
  export function liveCursorMaxModeBasesForTests(): ReadonlySet<string> {
@@ -42,6 +42,7 @@ export interface CursorCheckpointSnapshot {
42
42
  coveredMessageCount?: number;
43
43
  prefixDigest?: string;
44
44
  systemDigest?: string;
45
+ toolSuspended?: boolean;
45
46
  }
46
47
 
47
48
  interface CursorCheckpointStore {
@@ -229,6 +230,7 @@ export function commitCursorCheckpoint(input: {
229
230
  coveredMessageCount?: number;
230
231
  prefixDigest?: string;
231
232
  systemDigest?: string;
233
+ toolSuspended?: boolean;
232
234
  }): string | undefined {
233
235
  if (!input.conversationId || !input.modelId || input.checkpointBytes.byteLength === 0) return undefined;
234
236
  if (input.checkpointBytes.byteLength > CURSOR_CHECKPOINT_MAX_TOTAL_BYTES) return undefined;
@@ -258,6 +260,7 @@ export function commitCursorCheckpoint(input: {
258
260
  ...(input.coveredMessageCount !== undefined ? { coveredMessageCount: input.coveredMessageCount } : {}),
259
261
  ...(input.prefixDigest ? { prefixDigest: input.prefixDigest } : {}),
260
262
  ...(input.systemDigest ? { systemDigest: input.systemDigest } : {}),
263
+ ...(input.toolSuspended ? { toolSuspended: true } : {}),
261
264
  };
262
265
  const blobIds = collectCheckpointBlobIds(input.checkpointBytes);
263
266
  if (blobIds === undefined) return undefined;
@@ -305,15 +305,18 @@ export function isCursorRouterModelId(modelId: string): boolean {
305
305
  export function filterCursorConfiguredModelsByLiveDiscovery<T extends { id: string }>(
306
306
  configured: readonly T[],
307
307
  liveIds: readonly string[],
308
+ maxModeLiveIds: readonly string[] = [],
308
309
  ): T[] {
309
- return configured.filter(model =>
310
- !CURSOR_KNOWN_UNCALLABLE_MODEL_IDS.has(model.id)
311
- && (
312
- isCursorRouterModelId(model.id)
313
- // Synthetic ultra rows ride their base model's account availability.
314
- || isCursorModelAvailableForAccount(cursorUltraBaseModelId(model.id) ?? model.id, liveIds)
315
- ),
316
- );
310
+ return configured.filter(model => {
311
+ if (CURSOR_KNOWN_UNCALLABLE_MODEL_IDS.has(model.id)) return false;
312
+ if (isCursorRouterModelId(model.id)) return true;
313
+ const ultraBase = cursorUltraBaseModelId(model.id);
314
+ return isCursorModelAvailableForAccount(ultraBase ?? model.id, liveIds)
315
+ // Successful discovery is authoritative: synthetic ultra rows additionally require the
316
+ // account-specific Max Mode capability. Discovery failures bypass this filter and retain
317
+ // the static seed under the caller's existing degraded-catalog policy.
318
+ && (ultraBase === undefined || isCursorModelAvailableForAccount(ultraBase, maxModeLiveIds));
319
+ });
317
320
  }
318
321
 
319
322
  /**
@@ -87,7 +87,7 @@ import {
87
87
  type BackgroundShellTerminationReport,
88
88
  } from "./native-exec-shell";
89
89
  import type { CursorClientMessage, CursorRunRequest, CursorServerMessage } from "./types";
90
- import type { CursorTransport, CursorTransportFactoryInput } from "./transport";
90
+ import type { CursorStreamHealthClock, CursorTransport, CursorTransportFactoryInput } from "./transport";
91
91
  import { CursorHttp1BidiConnection } from "./http1-bidi";
92
92
  import { isPinnedHttp1 } from "../../lib/upstream-http-version";
93
93
 
@@ -107,6 +107,16 @@ const CURSOR_STREAM_SILENCE_FAIL_MS = 30_000;
107
107
  * Reset on every decoded frame that is not liveness-only.
108
108
  */
109
109
  const CURSOR_STREAM_HEARTBEAT_ONLY_FAIL_MS = 90_000;
110
+ /**
111
+ * The production T04 clock. The watchdog reads time and schedules its one timer through this
112
+ * object so a test can supply a clock it advances by hand; nothing else about the transport's
113
+ * timers moves with it.
114
+ */
115
+ const REAL_STREAM_HEALTH_CLOCK: CursorStreamHealthClock = {
116
+ now: () => Date.now(),
117
+ setTimeout: (callback, ms) => setTimeout(callback, ms),
118
+ clearTimeout: timer => clearTimeout(timer),
119
+ };
110
120
  /**
111
121
  * After `turnEnded` is decoded, the application turn is complete. A server that keeps
112
122
  * HTTP/2 open past this point cannot hold the turn hostage (senpi #1062): we close our side
@@ -494,6 +504,8 @@ class LiveCursorTransport implements CursorTransport {
494
504
  private lastInboundFrameAt = 0;
495
505
  private lastMeaningfulFrameAt = 0;
496
506
  private streamHealthFail?: (error: Error) => void;
507
+ /** Time source for the T04 watchdog only; the globals unless a test injects one. */
508
+ private readonly streamHealthClock: CursorStreamHealthClock;
497
509
  private committed = false;
498
510
  private expectedClose = false;
499
511
  /**
@@ -542,6 +554,7 @@ class LiveCursorTransport implements CursorTransport {
542
554
  // so the transport-level race test can drive it deterministically.
543
555
  this.clientToolFinalizeGraceMs = input.clientToolFinalizeGraceMs ?? CLIENT_TOOL_FINALIZE_GRACE_MS;
544
556
  this.activeClientToolFinalizeGraceMs = this.clientToolFinalizeGraceMs;
557
+ this.streamHealthClock = input.streamHealthClock ?? REAL_STREAM_HEALTH_CLOCK;
545
558
  // Desktop (computer-use / record-screen) executors are available even with no MCP servers.
546
559
  this.desktopDeps = desktopDepsFromConfig(input.provider.desktopExecutor);
547
560
  this.execContext = {
@@ -847,7 +860,7 @@ class LiveCursorTransport implements CursorTransport {
847
860
 
848
861
  private clearStreamHealthTimer(): void {
849
862
  if (this.streamHealthTimer) {
850
- clearTimeout(this.streamHealthTimer);
863
+ this.streamHealthClock.clearTimeout(this.streamHealthTimer);
851
864
  this.streamHealthTimer = undefined;
852
865
  }
853
866
  this.streamHealthFail = undefined;
@@ -858,24 +871,28 @@ class LiveCursorTransport implements CursorTransport {
858
871
  * failAndClear; the timer owns nothing else. Never armed before the first decoded
859
872
  * frame (the first-frame timer covers dial + first response), and disarmed by
860
873
  * every settle / expected-close path alongside the other timers.
874
+ *
875
+ * Both clocks and the timer go through `streamHealthClock`, which is the globals in
876
+ * production. The `elapsedMs` diagnostic below deliberately stays on the wall clock,
877
+ * because `turnStartedAt` is stamped there and the pair has to subtract coherently.
861
878
  */
862
879
  private armStreamHealthTimer(fail: (error: Error) => void): void {
863
- if (this.streamHealthTimer) clearTimeout(this.streamHealthTimer);
880
+ if (this.streamHealthTimer) this.streamHealthClock.clearTimeout(this.streamHealthTimer);
864
881
  if (this.expectedClose) return;
865
882
  this.streamHealthFail = fail;
866
883
  const silenceMs = this.input.streamSilenceFailMs ?? CURSOR_STREAM_SILENCE_FAIL_MS;
867
884
  const heartbeatOnlyMs = this.input.streamHeartbeatOnlyFailMs ?? CURSOR_STREAM_HEARTBEAT_ONLY_FAIL_MS;
868
- const now = Date.now();
885
+ const now = this.streamHealthClock.now();
869
886
  const deadline = Math.min(
870
887
  this.lastInboundFrameAt + silenceMs,
871
888
  this.lastMeaningfulFrameAt + heartbeatOnlyMs,
872
889
  );
873
- this.streamHealthTimer = setTimeout(() => {
890
+ this.streamHealthTimer = this.streamHealthClock.setTimeout(() => {
874
891
  this.streamHealthTimer = undefined;
875
892
  const failFn = this.streamHealthFail;
876
893
  if (!failFn || this.expectedClose) return;
877
- const stalledFor = Date.now() - this.lastInboundFrameAt;
878
- const meaningfulStalledFor = Date.now() - this.lastMeaningfulFrameAt;
894
+ const stalledFor = this.streamHealthClock.now() - this.lastInboundFrameAt;
895
+ const meaningfulStalledFor = this.streamHealthClock.now() - this.lastMeaningfulFrameAt;
879
896
  if (stalledFor < silenceMs && meaningfulStalledFor < heartbeatOnlyMs) {
880
897
  // A frame landed between arming and firing — re-arm for the fresh deadline.
881
898
  this.armStreamHealthTimer(failFn);
@@ -906,7 +923,7 @@ class LiveCursorTransport implements CursorTransport {
906
923
  * heartbeat-only threshold.
907
924
  */
908
925
  private noteInboundFrame(livenessOnly: boolean): void {
909
- const now = Date.now();
926
+ const now = this.streamHealthClock.now();
910
927
  this.lastInboundFrameAt = now;
911
928
  if (!livenessOnly) this.lastMeaningfulFrameAt = now;
912
929
  if (this.streamHealthFail) this.armStreamHealthTimer(this.streamHealthFail);
@@ -1288,7 +1305,7 @@ class LiveCursorTransport implements CursorTransport {
1288
1305
  const decodedUpdate = decoded.message.case === "interactionUpdate" ? decoded.message.value.message?.case : undefined;
1289
1306
  const livenessOnly = decodedUpdate === "heartbeat" || decoded.message.case === "conversationCheckpointUpdate";
1290
1307
  if (!this.streamHealthFail) {
1291
- const now = Date.now();
1308
+ const now = this.streamHealthClock.now();
1292
1309
  this.lastInboundFrameAt = now;
1293
1310
  this.lastMeaningfulFrameAt = now;
1294
1311
  this.streamHealthFail = failAndClear;
@@ -741,7 +741,7 @@ function contentText(message: OcxMessage): string {
741
741
  if (typeof message.content === "string") return message.content;
742
742
  return message.content
743
743
  .map(part => {
744
- if (part.type === "text") return part.text;
744
+ if (part.type === "text" || part.type === "document") return part.text;
745
745
  if (part.type === "thinking") return part.thinking;
746
746
  if (part.type === "image") return undefined;
747
747
  return undefined;
@@ -754,7 +754,7 @@ function contentToText(content: OcxToolResultMessage["content"]): string {
754
754
  if (typeof content === "string") return content;
755
755
  return content
756
756
  .map(part => {
757
- if (part.type === "text") return part.text;
757
+ if (part.type === "text" || part.type === "document") return part.text;
758
758
  if (part.type === "image") return CURSOR_VISION_IMAGE_HISTORY_MARKER;
759
759
  return undefined;
760
760
  })
@@ -767,7 +767,7 @@ function historyContentText(message: OcxMessage): string {
767
767
  if (message.role === "toolResult" || typeof message.content === "string") return contentText(message);
768
768
  return message.content
769
769
  .map(part => {
770
- if (part.type === "text") return part.text;
770
+ if (part.type === "text" || part.type === "document") return part.text;
771
771
  if (part.type === "thinking") return part.thinking;
772
772
  if (part.type === "image") return CURSOR_VISION_IMAGE_HISTORY_MARKER;
773
773
  return undefined;
@@ -836,6 +836,9 @@ function decodeResultParts(message: OcxToolResultMessage): DecodedResultPart[] |
836
836
  return content.map((part): DecodedResultPart => {
837
837
  if (part.type === "text") return { kind: "text", text: part.text };
838
838
  if (part.type === "video") return { kind: "text", text: "[video]" };
839
+ // Without this the document falls through to decodeInlineImage(part.imageUrl) below and is
840
+ // treated as an image it is not.
841
+ if (part.type === "document") return { kind: "text", text: part.text };
839
842
  const decoded = decodeInlineImage(part.imageUrl);
840
843
  return decoded ? { kind: "image", ...decoded } : { kind: "undecodable" };
841
844
  });
@@ -213,7 +213,7 @@ export function cursorRequestEmitsFastVariant(parsed: OcxParsedRequest): boolean
213
213
  * instead: current Cursor clients send the matching Grok base id plus `effort` and `fast` parameters.
214
214
  * A fully-qualified id (one that is not a known effort base) passes through unchanged.
215
215
  */
216
- function normalizeCursorModelId(modelId: string, reasoning?: string, fast?: boolean): {
216
+ function normalizeCursorModelId(modelId: string, reasoning?: string, fast?: boolean, liveRosterScope?: string): {
217
217
  modelId: string;
218
218
  requestedModelParameters?: readonly CursorRequestedModelParameter[];
219
219
  routingLevel?: CursorRoutingLevel;
@@ -239,7 +239,7 @@ function normalizeCursorModelId(modelId: string, reasoning?: string, fast?: bool
239
239
  ],
240
240
  };
241
241
  }
242
- const resolved = resolveCursorSelection(id, reasoning, undefined, { fast });
242
+ const resolved = resolveCursorSelection(id, reasoning, undefined, { fast, liveRosterScope });
243
243
  return {
244
244
  ...selection,
245
245
  ...(resolved.maxMode ? { maxMode: true } : {}),
@@ -251,6 +251,9 @@ function contentPartToText(part: OcxContentPart | OcxAssistantContentPart): stri
251
251
  switch (part.type) {
252
252
  case "text":
253
253
  return part.text;
254
+ case "document":
255
+ // Cursor has no document carrier; the marker keeps the turn from serializing to nothing.
256
+ return part.text;
254
257
  case "thinking":
255
258
  return part.thinking;
256
259
  case "image":
@@ -407,6 +410,8 @@ export function cursorCoveredPrefixDigest(parsed: OcxParsedRequest, coveredMessa
407
410
  export interface CreateCursorRequestOptions {
408
411
  /** Force a brand-new Cursor conversation id even when remembered state exists. */
409
412
  forceFreshConversation?: boolean;
413
+ /** Credential-bound scope for live Cursor model spelling and Max-Mode evidence. */
414
+ liveRosterScope?: string;
410
415
  }
411
416
 
412
417
  function lookupPrefixSnapshot(
@@ -476,7 +481,13 @@ function resolveCursorCheckpoint(
476
481
  if (cursorCheckpointModelAffinityId(snapshot.modelId) !== cursorCheckpointModelAffinityId(request.modelId)) {
477
482
  return { reason: "model_changed" };
478
483
  }
479
- if (parsed.context.messages.at(-1)?.role !== "toolResult" && cursorState?.checkpointUsable === false) {
484
+ // The continuation state only exists on the ref path; a ref-less prefix hit carries
485
+ // the suspension on the snapshot itself, or a non-toolResult request could resume
486
+ // bytes upstream serialized mid-tool-call.
487
+ if (
488
+ parsed.context.messages.at(-1)?.role !== "toolResult"
489
+ && (snapshot.toolSuspended === true || cursorState?.checkpointUsable === false)
490
+ ) {
480
491
  return { reason: "trailing_tool_result" };
481
492
  }
482
493
  const lineage = lineageMismatch(parsed, snapshot);
@@ -493,7 +504,12 @@ export function createCursorRequest(
493
504
  const visibleTools = cursorToolsForActivePrompt(parsed.context.tools, activeText, parsed.options.toolChoice);
494
505
  const budget = applyCursorToolBudget(visibleTools, parsed.options.toolChoice);
495
506
  const limitNote = catalogLimitNote(budget.tools, budget.omitted);
496
- const model = normalizeCursorModelId(parsed.modelId, parsed.options.reasoning, cursorFastRequested(parsed));
507
+ const model = normalizeCursorModelId(
508
+ parsed.modelId,
509
+ parsed.options.reasoning,
510
+ cursorFastRequested(parsed),
511
+ options.liveRosterScope,
512
+ );
497
513
  const request: CursorRunRequest = {
498
514
  modelId: model.modelId,
499
515
  ...(model.requestedModelParameters ? { requestedModelParameters: model.requestedModelParameters } : {}),
@@ -55,6 +55,25 @@ export interface CursorTransportFactoryInput {
55
55
  * client thread. Distinct from the per-transport native-exec/shell owner.
56
56
  */
57
57
  sessionId?: string;
58
+ /**
59
+ * Test-only clock for the T04 inbound stream-health watchdog. Production omits it and the
60
+ * transport uses the global timers; injecting here must not change scheduling semantics.
61
+ *
62
+ * It exists because the watchdog's contract — a meaningful frame re-arms both deadlines — can
63
+ * only be asserted against real timers by keeping a synthetic server delivering frames faster
64
+ * than the silence budget for several multiples of it. That is an assertion about how busy the
65
+ * machine is, and it failed in the unsharded macOS lane for exactly that reason while the
66
+ * watchdog was working correctly. With the clock injected, virtual time advances only where
67
+ * the test says it does, so no amount of runner contention can expire a deadline.
68
+ */
69
+ streamHealthClock?: CursorStreamHealthClock;
70
+ }
71
+
72
+ /** The three time primitives the T04 watchdog uses. Production binds all three to the globals. */
73
+ export interface CursorStreamHealthClock {
74
+ now(): number;
75
+ setTimeout(callback: () => void, ms: number): ReturnType<typeof setTimeout>;
76
+ clearTimeout(timer: ReturnType<typeof setTimeout>): void;
58
77
  }
59
78
 
60
79
  export type CursorTransportFactory = (input: CursorTransportFactoryInput) => CursorTransport;
@@ -61,6 +61,7 @@ import {
61
61
  CursorTransportDisabledError,
62
62
  type CursorTransportFactory,
63
63
  } from "./cursor/transport";
64
+ import { cursorLiveRosterScope } from "./cursor/catalog";
64
65
 
65
66
  export const CURSOR_API_URL = "https://api2.cursor.sh";
66
67
 
@@ -172,9 +173,11 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
172
173
  // Namespace thread→conversation derivation by the authenticated Cursor credential so
173
174
  // shared-proxy tenants with different Cursor accounts cannot collide on a parent thread id.
174
175
  // Prefer an already-set auth scope (e.g. Codex pool account) when present.
176
+ let liveRosterScope: string | undefined;
175
177
  if (!_parsed._cursorIdentityScope) {
176
178
  try {
177
179
  const token = resolveCursorToken(provider, incoming.headers);
180
+ liveRosterScope = cursorLiveRosterScope(provider.baseUrl, token);
178
181
  _parsed._cursorIdentityScope = createHash("sha256")
179
182
  .update("ocx:cursor:acct:")
180
183
  .update(token)
@@ -183,11 +186,17 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
183
186
  } catch {
184
187
  /* Missing credential is handled by the live transport path below. */
185
188
  }
189
+ } else {
190
+ try {
191
+ liveRosterScope = cursorLiveRosterScope(provider.baseUrl, resolveCursorToken(provider, incoming.headers));
192
+ } catch {
193
+ /* Missing credential is handled by the live transport path below. */
194
+ }
186
195
  }
187
196
  const inheritedCheckpointRef = _parsed._providerContinuation?.cursor?.checkpointRef;
188
197
  const previousConversationId = _parsed._cursorConversationId;
189
198
  let request = {
190
- ...createCursorRequest(_parsed),
199
+ ...createCursorRequest(_parsed, { liveRosterScope }),
191
200
  _cursorIdentityScope: _parsed._cursorIdentityScope?.trim() || "local",
192
201
  };
193
202
  requestSizeContext = cursorRequestSizeContext(request);
@@ -254,6 +263,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
254
263
  coveredMessageCount,
255
264
  prefixDigest: cursorCoveredPrefixDigest(_parsed, coveredMessageCount),
256
265
  systemDigest: cursorInstructionDigest(_parsed),
266
+ toolSuspended: toolSuspendedCommit,
257
267
  });
258
268
  if (!checkpointRef) return;
259
269
  if (previousRef && previousRef !== checkpointRef) invalidateCursorCheckpoint(previousRef);
@@ -263,7 +273,8 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
263
273
  ...(_parsed._providerContinuation?.cursor ?? {}),
264
274
  conversationId: activeRequest.conversationId,
265
275
  // A tool-suspended checkpoint is only usable by the immediate trailing-toolResult
266
- // continuation; the request-builder guard keys on checkpointUsable=false for that.
276
+ // continuation; the request-builder guard keys on this checkpointUsable=false and
277
+ // the snapshot's persisted toolSuspended flag for that.
267
278
  checkpointUsable: !toolSuspendedCommit,
268
279
  checkpointRef,
269
280
  },
@@ -439,7 +450,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
439
450
  lastTransport = undefined;
440
451
  _parsed._cursorConversationId = undefined;
441
452
  const next = {
442
- ...createCursorRequest(_parsed, { forceFreshConversation: true }),
453
+ ...createCursorRequest(_parsed, { forceFreshConversation: true, liveRosterScope }),
443
454
  _cursorIdentityScope: _parsed._cursorIdentityScope?.trim() || "local",
444
455
  };
445
456
  rekeyContextUsage(failedConversationId, next.conversationId);
@@ -0,0 +1,45 @@
1
+ // Type-only: erased at compile time, so this does not create an import cycle with the registry.
2
+ import type { AdapterWire } from "./registry";
3
+ import type { OcxMessage, OcxParsedRequest } from "../types";
4
+ import { toolRestrictsCallers } from "../types";
5
+
6
+ /**
7
+ * Wires that can actually carry a constraint, as a default-deny allowlist.
8
+ *
9
+ * A per-adapter opt-in is the wrong shape for this: an adapter that never learned about a
10
+ * carrier rebuilds the declaration or the message without it and returns a normal completion,
11
+ * which is exactly the silent widening this batch exists to remove. Listing the wires that CAN
12
+ * carry it means a new wire refuses until someone teaches it, and adding a member to
13
+ * `AdapterWire` makes the omission visible here rather than at a customer's upstream.
14
+ */
15
+ const CALLER_RESTRICTION_WIRES: ReadonlySet<AdapterWire> = new Set<AdapterWire>(["anthropic"]);
16
+ const INLINE_DOCUMENT_WIRES: ReadonlySet<AdapterWire> = new Set<AdapterWire>([
17
+ "anthropic",
18
+ "openai-chat",
19
+ "google",
20
+ ]);
21
+
22
+ /** A fixed-vocabulary refusal, or `undefined` when this wire can hold everything the request carries. */
23
+ export function unrepresentableDeclaration(parsed: OcxParsedRequest, wire: AdapterWire): string | undefined {
24
+ if (!CALLER_RESTRICTION_WIRES.has(wire) && parsed.context.tools?.some(toolRestrictsCallers)) {
25
+ // The name is deliberately absent: it is caller-controlled and would put client metadata
26
+ // into an error body.
27
+ return "OpenCodex cannot express tools[].allowed_callers on this route. "
28
+ + "Route the request to an Anthropic-protocol provider, or remove the caller restriction.";
29
+ }
30
+ if (!INLINE_DOCUMENT_WIRES.has(wire) && parsed.context.messages.some(carriesDocument)) {
31
+ return "OpenCodex cannot translate document input on this route. "
32
+ + "Use a native input wire that supports the attachment, or convert it to text first.";
33
+ }
34
+ return undefined;
35
+ }
36
+
37
+ /**
38
+ * Typed structurally rather than as `OcxContentPart[]`: an assistant turn's content is
39
+ * `OcxAssistantContentPart[]`, which carries thinking and tool-call members and is not
40
+ * assignable to the user-content union. Only the discriminant is read here.
41
+ */
42
+ function carriesDocument(message: OcxMessage): boolean {
43
+ const content: unknown = message.content;
44
+ return Array.isArray(content) && (content as ReadonlyArray<{ type: string }>).some(part => part.type === "document");
45
+ }
@@ -1055,6 +1055,8 @@ export interface CloudChatRequest {
1055
1055
  catalog?: CacheEntry | null;
1056
1056
  /** Abort signal — closes the fetch stream. */
1057
1057
  signal?: AbortSignal;
1058
+ /** Executor for the inference POST only; catalog and JWT RPCs retain their own transport. */
1059
+ executor?: typeof globalThis.fetch;
1058
1060
  }
1059
1061
 
1060
1062
  export class CloudChatError extends Error {
@@ -1216,7 +1218,7 @@ export async function* streamChatEvents(req: CloudChatRequest): AsyncGenerator<C
1216
1218
 
1217
1219
  let resp: Response;
1218
1220
  try {
1219
- resp = await fetch(`${host}/exa.api_server_pb.ApiServerService/GetChatMessage`, {
1221
+ resp = await (req.executor ?? globalThis.fetch)(`${host}/exa.api_server_pb.ApiServerService/GetChatMessage`, {
1220
1222
  method: 'POST',
1221
1223
  headers: {
1222
1224
  'Content-Type': 'application/connect+proto',
@@ -5,7 +5,9 @@
5
5
  * is idempotent; ambiguous transport failures still propagate unchanged.
6
6
  */
7
7
  import { parseRetryAfterFromMessage } from '../../../lib/retry-delay.js';
8
- import { abortError, sleepWithAbort } from '../../../lib/upstream-retry.js';
8
+ import type { AdapterFetchContext } from '../../base.js';
9
+ import { createAdapterPhysicalSend } from '../../physical-send.js';
10
+ import { abortError, SendBudgetExhaustedError, sleepWithAbort } from '../../../lib/upstream-retry.js';
9
11
  import { CloudChatError, streamChatEvents, type CloudChatEvent, type CloudChatRequest } from './chat.js';
10
12
 
11
13
  /** 1 initial attempt plus at most 2 replays. */
@@ -33,6 +35,11 @@ export interface StatedResetRetryOptions {
33
35
  maxReplays?: number;
34
36
  /** CUMULATIVE wait allowance, not a fresh allowance on every failure. */
35
37
  maxWaitMs?: number;
38
+ /** Request-wide execution authority. Absent preserves context-free callers' unlimited fetch. */
39
+ execution?: Pick<
40
+ AdapterFetchContext,
41
+ "executor" | "sendBudget" | "onPhysicalSend" | "onRecoveryWithheld"
42
+ >;
36
43
  }
37
44
 
38
45
  function replayLimit(value: number | undefined): number {
@@ -59,15 +66,34 @@ export async function* streamChatEventsWithResetRetry(
59
66
  const sleep = options?.sleep ?? sleepWithAbort;
60
67
  const maxReplays = replayLimit(options?.maxReplays);
61
68
  const maxWaitMs = waitLimit(options?.maxWaitMs);
69
+ const execution = options?.execution;
70
+ // One sender owns the whole invocation so ordinals span the initial POST and both replays.
71
+ // Its executor remains lazy: replay admission happens after the provider-stated wait, never
72
+ // while a reservation could be held for up to an hour.
73
+ const send = execution
74
+ ? createAdapterPhysicalSend({ ...execution, abortSignal: req.signal })
75
+ : undefined;
62
76
  let replays = 0;
63
77
  let waitedMs = 0;
78
+ let replaySourceError: CloudChatError | undefined;
64
79
  while (true) {
65
80
  // Check again after sleeping: cancellation can race with timer completion.
66
81
  // A pre-aborted request must not even enter a custom transport.
67
82
  if (req.signal?.aborted) throw abortError(req.signal);
68
83
  let yielded = false;
69
84
  try {
70
- for await (const event of stream(req)) {
85
+ const recovery = replays > 0 ? "rate-limit-429" as const : undefined;
86
+ const attemptRequest = send
87
+ ? {
88
+ ...req,
89
+ executor: ((input, init) => send({
90
+ url: typeof input === "string" ? input : input instanceof URL ? input.href : input.url,
91
+ ...(recovery ? { sendClass: "auth-recovery" as const, recovery } : {}),
92
+ dispatch: executor => executor(input, init),
93
+ })) as typeof globalThis.fetch,
94
+ }
95
+ : req;
96
+ for await (const event of stream(attemptRequest)) {
71
97
  // Latch before yielding, so a consumer-injected error is post-output.
72
98
  yielded = true;
73
99
  yield event;
@@ -75,14 +101,24 @@ export async function* streamChatEventsWithResetRetry(
75
101
  return;
76
102
  } catch (error) {
77
103
  if (req.signal?.aborted) throw abortError(req.signal);
78
- const waitSec = !yielded
104
+ if (!yielded && error instanceof SendBudgetExhaustedError && replaySourceError) {
105
+ // The provider's refusal is the real upstream answer. A local cap can withhold its
106
+ // recovery, but replacing the 429 would erase the status and stated reset metadata.
107
+ execution?.onRecoveryWithheld?.({ reason: "retry-send-budget" });
108
+ throw replaySourceError;
109
+ }
110
+ const retryableError = !yielded
79
111
  && error instanceof CloudChatError
80
112
  && error.status === 429
81
- ? parseRetryAfterFromMessage(error.message)
113
+ ? error
114
+ : undefined;
115
+ const waitSec = retryableError
116
+ ? parseRetryAfterFromMessage(retryableError.message)
82
117
  : undefined;
83
118
  const waitMs = waitSec === undefined ? undefined : waitSec * 1000;
84
119
  if (
85
- waitMs === undefined
120
+ retryableError === undefined
121
+ || waitMs === undefined
86
122
  || replays >= maxReplays
87
123
  || waitMs > maxWaitMs - waitedMs
88
124
  ) {
@@ -91,6 +127,7 @@ export async function* streamChatEventsWithResetRetry(
91
127
  throw error;
92
128
  }
93
129
  replays += 1;
130
+ replaySourceError = retryableError;
94
131
  // Charge the complete scheduled wait once, before sleeping. This is a
95
132
  // sleep allowance, not a wall-clock deadline on generation or timer
96
133
  // scheduling: waking a few milliseconds late must not reject an already