@bitkyc08/opencodex 2.58.0 → 2.60.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 (303) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  3. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/crusoe.svg +1 -0
  6. package/gui/dist/provider-icons/opper.svg +3 -0
  7. package/package.json +4 -1
  8. package/src/adapters/anthropic-image-codec.ts +16 -2
  9. package/src/adapters/anthropic-image-normalize.ts +49 -2
  10. package/src/adapters/anthropic.ts +4 -1
  11. package/src/adapters/base.ts +34 -1
  12. package/src/adapters/coding-agent/turn.ts +22 -2
  13. package/src/adapters/command-code.ts +50 -3
  14. package/src/adapters/cursor/catalog.ts +11 -0
  15. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  16. package/src/adapters/cursor/discovery.ts +11 -8
  17. package/src/adapters/cursor/effort-map.ts +16 -2
  18. package/src/adapters/cursor/envelope-echo.ts +55 -2
  19. package/src/adapters/cursor/live-transport.ts +26 -9
  20. package/src/adapters/cursor/message-mapper.ts +3 -2
  21. package/src/adapters/cursor/protobuf-request.ts +8 -5
  22. package/src/adapters/cursor/request-builder.ts +21 -4
  23. package/src/adapters/cursor/thread-continuity.ts +105 -31
  24. package/src/adapters/cursor/tool-guidance.ts +5 -4
  25. package/src/adapters/cursor/transport.ts +19 -0
  26. package/src/adapters/cursor.ts +45 -2
  27. package/src/adapters/devin/cloud-direct/chat.ts +14 -3
  28. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  29. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
  30. package/src/adapters/devin.ts +125 -26
  31. package/src/adapters/google-antigravity-replay.ts +1 -1
  32. package/src/adapters/google-antigravity-wire.ts +55 -4
  33. package/src/adapters/google-http.ts +57 -11
  34. package/src/adapters/google-tool-schema.ts +595 -31
  35. package/src/adapters/google-wire-compiler.ts +93 -10
  36. package/src/adapters/google-wire-shape.ts +461 -0
  37. package/src/adapters/google.ts +60 -10
  38. package/src/adapters/openai-chat/response-events.ts +61 -0
  39. package/src/adapters/openai-chat-images.ts +3 -1
  40. package/src/adapters/openai-chat.ts +10 -11
  41. package/src/adapters/openai-responses/image-gen.ts +8 -6
  42. package/src/adapters/openai-responses/passthrough.ts +25 -4
  43. package/src/adapters/openai-responses/reasoning.ts +7 -0
  44. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  45. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  46. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  47. package/src/adapters/responses-tool-schema.ts +76 -46
  48. package/src/adapters/run-turn-queue.ts +17 -4
  49. package/src/bridge/response-json.ts +1 -1
  50. package/src/bridge/sse.ts +28 -35
  51. package/src/claude/context-windows.ts +22 -0
  52. package/src/claude/outbound.ts +35 -4
  53. package/src/cli/account-api.ts +4 -3
  54. package/src/cli/account-extended.ts +26 -6
  55. package/src/cli/account-orca-import.ts +63 -0
  56. package/src/cli/account.ts +32 -4
  57. package/src/cli/capabilities.ts +40 -0
  58. package/src/cli/claude.ts +29 -1
  59. package/src/cli/codex-cli-update.ts +97 -2
  60. package/src/cli/dispatch.ts +57 -3
  61. package/src/cli/doctor.ts +218 -4
  62. package/src/cli/help.ts +4 -1
  63. package/src/cli/hub.ts +3 -2
  64. package/src/cli/index.ts +95 -22
  65. package/src/cli/models-runtime.ts +33 -4
  66. package/src/cli/opencode.ts +2 -2
  67. package/src/cli/provider.ts +13 -1
  68. package/src/cli/registry.ts +11 -1
  69. package/src/cli/runtime-api.ts +44 -0
  70. package/src/cli/start-args.ts +94 -0
  71. package/src/cli/system-command.ts +2 -0
  72. package/src/client/machine-api.ts +6 -5
  73. package/src/client/machine-listener.ts +16 -3
  74. package/src/client/runtime.ts +26 -2
  75. package/src/clients/config-export/constants.ts +2 -3
  76. package/src/clients/config-export.ts +5 -5
  77. package/src/codex/account-store.ts +146 -5
  78. package/src/codex/auth-api/account-list.ts +19 -11
  79. package/src/codex/auth-api/pool-quota-probe.ts +44 -10
  80. package/src/codex/auth-api/routes.ts +17 -2
  81. package/src/codex/auth-context.ts +16 -12
  82. package/src/codex/catalog/build-entries.ts +25 -4
  83. package/src/codex/catalog/derive-entry.ts +8 -1
  84. package/src/codex/catalog/effort.ts +10 -6
  85. package/src/codex/catalog/gather-capture.ts +22 -2
  86. package/src/codex/catalog/model-hints.ts +66 -33
  87. package/src/codex/catalog/parsing.ts +90 -5
  88. package/src/codex/catalog/provider-models.ts +19 -2
  89. package/src/codex/catalog/reserve-warn.ts +96 -0
  90. package/src/codex/catalog/retained-sync.ts +41 -26
  91. package/src/codex/catalog/routed-gather.ts +61 -3
  92. package/src/codex/cli-installation-identity.ts +210 -0
  93. package/src/codex/cli-installation-targets.ts +158 -0
  94. package/src/codex/context-compat.ts +5 -2
  95. package/src/codex/convergence.ts +5 -0
  96. package/src/codex/desired-state.ts +4 -1
  97. package/src/codex/history-job.ts +6 -6
  98. package/src/codex/history-provider.ts +24 -167
  99. package/src/codex/history-rollout-read.ts +174 -0
  100. package/src/codex/history-state-open.ts +105 -0
  101. package/src/codex/inject/config-toml.ts +44 -2
  102. package/src/codex/inject.ts +3 -2
  103. package/src/codex/internal/catalog-writer.ts +33 -1
  104. package/src/codex/lineage.ts +83 -32
  105. package/src/codex/loopback-target.ts +31 -0
  106. package/src/codex/main-account-hard-lock.ts +2 -1
  107. package/src/codex/main-account.ts +10 -3
  108. package/src/codex/main-device-reauth.ts +17 -9
  109. package/src/codex/model-cache.ts +47 -0
  110. package/src/codex/model-entitlements.ts +80 -2
  111. package/src/codex/observed-model-denials.ts +230 -0
  112. package/src/codex/orca-auth-source.ts +94 -0
  113. package/src/codex/orca-import.ts +219 -0
  114. package/src/codex/prompt-text-probe.ts +289 -16
  115. package/src/codex/quota-401-recovery.ts +12 -0
  116. package/src/codex/quota-types.ts +65 -0
  117. package/src/codex/quota.ts +24 -19
  118. package/src/codex/routing/cooldown-math.ts +8 -47
  119. package/src/codex/routing/pin-drain.ts +57 -0
  120. package/src/codex/routing.ts +20 -16
  121. package/src/codex/shim.ts +1 -1
  122. package/src/codex/subagent-model-fallback.ts +114 -2
  123. package/src/codex/windows-installation-files.ts +224 -0
  124. package/src/combos/failover.ts +125 -5
  125. package/src/config/admitted-identity.ts +222 -0
  126. package/src/config/diagnostics.ts +43 -1
  127. package/src/config/feature-flags.ts +5 -0
  128. package/src/config/load-degrade.ts +33 -0
  129. package/src/config/pending-teardown.ts +8 -0
  130. package/src/config/process-state.ts +36 -3
  131. package/src/config/provider-relative-send-path.ts +16 -0
  132. package/src/config/proxy-env.ts +31 -7
  133. package/src/config/schema/compaction-triggers.ts +11 -0
  134. package/src/config/schema/config-schema.ts +25 -0
  135. package/src/config/schema/leaf-validators.ts +76 -17
  136. package/src/config.ts +2 -2
  137. package/src/generated/compatibility-version.json +410 -258
  138. package/src/generated/model-metadata.ts +1 -1
  139. package/src/grok/reset-coupons.ts +38 -19
  140. package/src/images/loop.ts +6 -1
  141. package/src/integrations/aside-profile-context.ts +37 -3
  142. package/src/integrations/aside-profile-journal.ts +68 -3
  143. package/src/integrations/aside-profiles.ts +128 -3
  144. package/src/integrations/mutation-plan.ts +815 -0
  145. package/src/integrations/writer.ts +85 -99
  146. package/src/lab/live/transport.ts +4 -0
  147. package/src/lab/live/types.ts +5 -0
  148. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  149. package/src/lib/admin-secrets.ts +9 -1
  150. package/src/lib/bounded-body.ts +4 -2
  151. package/src/lib/debug-log-buffer.ts +6 -1
  152. package/src/lib/debug.ts +23 -0
  153. package/src/lib/destination-policy.ts +48 -6
  154. package/src/lib/errors.ts +82 -15
  155. package/src/lib/http-response-semantics.ts +57 -0
  156. package/src/lib/lab-live-pinned-sender.ts +26 -12
  157. package/src/lib/local-destinations.ts +32 -5
  158. package/src/lib/pinned-http.ts +142 -2
  159. package/src/lib/plain-data.ts +103 -0
  160. package/src/lib/process-control.ts +13 -5
  161. package/src/lib/provider-outbound.ts +53 -5
  162. package/src/lib/proxy-env.ts +70 -3
  163. package/src/lib/request-execution-budget.ts +11 -3
  164. package/src/lib/response-body-inactivity.ts +193 -0
  165. package/src/lib/retry-delay.ts +69 -0
  166. package/src/lib/socks5-fetch.ts +741 -0
  167. package/src/lib/spend-ledger-owner.ts +364 -0
  168. package/src/lib/spend-reservation-ledger.ts +332 -35
  169. package/src/lib/windows-system-proxy.ts +16 -11
  170. package/src/lib/workflow-budget.ts +145 -8
  171. package/src/oauth/account-quota-rank.ts +72 -15
  172. package/src/oauth/callback-server.ts +4 -3
  173. package/src/oauth/generic-account-failover.ts +41 -27
  174. package/src/oauth/health.ts +12 -1
  175. package/src/oauth/index.ts +3 -107
  176. package/src/oauth/login-flow-state.ts +127 -0
  177. package/src/oauth/orcarouter.ts +15 -2
  178. package/src/oauth/store.ts +8 -0
  179. package/src/providers/codex-capacity.ts +9 -0
  180. package/src/providers/derive.ts +34 -17
  181. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  182. package/src/providers/devin-provider-merge-migration.ts +33 -12
  183. package/src/providers/free-directory.ts +20 -2
  184. package/src/providers/key-failover.ts +327 -25
  185. package/src/providers/model-rename-migration.ts +56 -1
  186. package/src/providers/model-rename-startup.ts +7 -5
  187. package/src/providers/openai-sidecar.ts +4 -0
  188. package/src/providers/openai-virtual-models.ts +42 -2
  189. package/src/providers/opencode-go-transport.ts +14 -5
  190. package/src/providers/quota/antigravity.ts +22 -2
  191. package/src/providers/quota/report-cache.ts +3 -0
  192. package/src/providers/quota/vendor-probes-key.ts +1 -1
  193. package/src/providers/registry/entries-core.ts +39 -17
  194. package/src/providers/registry/entries-extended.ts +131 -1
  195. package/src/providers/registry/model-ids.ts +168 -0
  196. package/src/providers/registry/model-seeds.ts +87 -21
  197. package/src/providers/registry/types.ts +2 -0
  198. package/src/providers/resolved-model-policy-merge.ts +167 -0
  199. package/src/providers/resolved-model-policy.ts +406 -0
  200. package/src/providers/stale-vision-classification-migration.ts +137 -0
  201. package/src/responses/apply-patch-envelope.ts +32 -11
  202. package/src/responses/bridge-search-replay-cache.ts +152 -0
  203. package/src/responses/code-mode-helper-compat.ts +26 -16
  204. package/src/responses/custom-tool-compat.ts +1 -1
  205. package/src/responses/freeform-wrapper-scan.ts +279 -0
  206. package/src/responses/hosted-tool-policy.ts +85 -2
  207. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  208. package/src/responses/progressive-freeform-input.ts +130 -0
  209. package/src/responses/reasoning-envelope.ts +30 -0
  210. package/src/responses/schema.ts +9 -2
  211. package/src/responses/state.ts +5 -12
  212. package/src/responses/tool-name-aliases.ts +15 -1
  213. package/src/router.ts +91 -115
  214. package/src/routing/compatibility/behavior.ts +9 -0
  215. package/src/routing/compatibility/subject.ts +16 -1
  216. package/src/server/adapter-resolve.ts +9 -0
  217. package/src/server/auth-cors.ts +29 -0
  218. package/src/server/chat-completions.ts +13 -5
  219. package/src/server/chat-native-sse.ts +26 -9
  220. package/src/server/chat-native.ts +10 -4
  221. package/src/server/claude-messages.ts +30 -5
  222. package/src/server/effort-row.ts +11 -3
  223. package/src/server/grok-responses-control-frame.ts +160 -1
  224. package/src/server/gui-static.ts +36 -2
  225. package/src/server/inbound-body-admission.ts +187 -0
  226. package/src/server/index/serve-options.ts +86 -29
  227. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  228. package/src/server/index/websocket-handler.ts +6 -1
  229. package/src/server/index.ts +24 -25
  230. package/src/server/management/api-access.ts +3 -4
  231. package/src/server/management/aside-profile-routes.ts +266 -7
  232. package/src/server/management/config-routes.ts +48 -7
  233. package/src/server/management/context.ts +3 -0
  234. package/src/server/management/integration-routes.ts +287 -5
  235. package/src/server/management/metrics-routes.ts +20 -0
  236. package/src/server/management/model-rows.ts +224 -12
  237. package/src/server/management/provider-capability-config.ts +35 -7
  238. package/src/server/management/provider-routes.ts +70 -18
  239. package/src/server/management/route-registry.ts +14 -0
  240. package/src/server/management/shared.ts +10 -3
  241. package/src/server/management/system-restart.ts +7 -2
  242. package/src/server/management/system-routes.ts +2 -0
  243. package/src/server/management/usage-aggregate-cache.ts +4 -0
  244. package/src/server/management-api.ts +2 -0
  245. package/src/server/management-auth.ts +15 -1
  246. package/src/server/proxy-liveness.ts +97 -2
  247. package/src/server/readiness.ts +29 -10
  248. package/src/server/relay-eager.ts +24 -2
  249. package/src/server/relay.ts +136 -34
  250. package/src/server/request-log.ts +80 -3
  251. package/src/server/request-metrics.ts +236 -0
  252. package/src/server/responses/adapter-continuation.ts +74 -30
  253. package/src/server/responses/adapter-delivery.ts +39 -8
  254. package/src/server/responses/adapter-dispatch.ts +63 -30
  255. package/src/server/responses/compact.ts +89 -18
  256. package/src/server/responses/compaction-routing.ts +111 -0
  257. package/src/server/responses/core-codex-account.ts +90 -24
  258. package/src/server/responses/core-combo.ts +7 -7
  259. package/src/server/responses/core-normalize.ts +16 -15
  260. package/src/server/responses/core-opaque-recovery.ts +1 -0
  261. package/src/server/responses/core-options.ts +4 -0
  262. package/src/server/responses/encrypted-payload.ts +20 -2
  263. package/src/server/responses/fetch-helpers.ts +68 -2
  264. package/src/server/responses/passthrough-delivery.ts +41 -15
  265. package/src/server/responses/passthrough-dispatch.ts +145 -53
  266. package/src/server/responses/passthrough-execution.ts +11 -1
  267. package/src/server/responses/policy-fallback.ts +5 -13
  268. package/src/server/responses/request-prepare.ts +96 -17
  269. package/src/server/responses/request-send-budget.ts +89 -8
  270. package/src/server/responses/request-sidecar-auth.ts +17 -9
  271. package/src/server/responses/request-spend.ts +38 -9
  272. package/src/server/responses/request-transport.ts +15 -12
  273. package/src/server/responses/run-turn-execution.ts +45 -8
  274. package/src/server/responses/sidecar-execution.ts +19 -2
  275. package/src/server/responses/ws-upstream.ts +16 -28
  276. package/src/server/responses-custom-tool-repair.ts +29 -56
  277. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  278. package/src/server/sse-frame-buffer.ts +12 -10
  279. package/src/server/sse-payload-rewrite.ts +37 -10
  280. package/src/server/system-env-shell.ts +5 -1
  281. package/src/server/system-env.ts +7 -1
  282. package/src/server/workflow-refusal.ts +56 -2
  283. package/src/service/cli.ts +16 -6
  284. package/src/service/guards.ts +10 -0
  285. package/src/service/health.ts +43 -0
  286. package/src/service/state.ts +7 -2
  287. package/src/tray/windows-tray.ps1 +155 -3
  288. package/src/types/accounts.ts +4 -0
  289. package/src/types/config.ts +111 -6
  290. package/src/types/provider.ts +37 -0
  291. package/src/types/request.ts +9 -1
  292. package/src/types/tools.ts +14 -0
  293. package/src/types/wire.ts +9 -1
  294. package/src/types.ts +1 -0
  295. package/src/usage/expected-prices.ts +28 -0
  296. package/src/usage/log.ts +87 -4
  297. package/src/vision/eligibility.ts +88 -9
  298. package/src/vision/plan.ts +34 -10
  299. package/src/web-search/executor.ts +41 -2
  300. package/src/web-search/loop.ts +6 -1
  301. package/src/web-search/passthrough-bridge.ts +39 -5
  302. package/gui/dist/assets/index-BbrHOIY0.js +0 -128
  303. package/gui/dist/assets/index-C5-RdDmD.css +0 -1
@@ -16,8 +16,8 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-BbrHOIY0.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-C5-RdDmD.css">
19
+ <script type="module" crossorigin src="/assets/index-DoBVdPHP.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/assets/index-BTuCbqQd.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
@@ -0,0 +1 @@
1
+ <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Crusoe</title><path d="M12 0L4.583 6.583c-3.23 2.869-3.23 7.965 0 10.834L12 24l7.417-6.583c3.23-2.869 3.23-7.965 0-10.834L12 0z" fill="url(#lobe-icons-crusoe-_R_0_)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-crusoe-_R_0_" x1="18.919" x2="4.853" y1="5.595" y2="18.301"><stop stop-color="#F4BF45"></stop><stop offset=".35" stop-color="#E48047"></stop><stop offset=".69" stop-color="#C73361"></stop><stop offset="1" stop-color="#A42F5F"></stop></linearGradient></defs></svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 315 315" fill="#000000">
2
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M159.78 315C71.53 315 0 244.49 0 157.5C0 -18.9499 159.78 0.650075 159.78 0.650075C159.78 87.2201 88.36 157.4 0.2 157.5C149.8 157.64 159.78 315 159.78 315ZM160.52 217.98C160.52 217.98 156.94 161.65 105.04 157.52C120.6 157.34 160.52 151.54 160.52 96.5601C160.52 151.54 200.44 157.34 216 157.52C164.1 161.63 160.52 217.98 160.52 217.98Z"/>
3
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.58.0",
3
+ "version": "2.60.0",
4
4
  "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -83,6 +83,9 @@
83
83
  "@types/bun": "1.4.0",
84
84
  "typescript": "7.0.2"
85
85
  },
86
+ "optionalDependencies": {
87
+ "wreq-js": "2.3.1"
88
+ },
86
89
  "overrides": {
87
90
  "@hono/node-server": "2.1.0",
88
91
  "fast-uri": "^3.1.7",
@@ -41,7 +41,9 @@ export const MAX_INPUT_BASE64_LENGTH = 64 * MiB;
41
41
  * would widen the adapter contract with no demonstrated need.
42
42
  */
43
43
  export const IMAGE_NORMALIZE_CONCURRENCY = 4;
44
- export const MAX_INPUT_PIXELS = 100_000_000;
44
+ // Keep one decoded RGBA surface below 64 MiB. Native codecs may allocate additional
45
+ // working buffers, so this is paired with process-wide admission in the normalizer.
46
+ export const MAX_INPUT_PIXELS = 16_000_000;
45
47
 
46
48
 
47
49
  /** Formats Anthropic accepts as-is; anything else must be transcoded or dropped. */
@@ -50,6 +52,8 @@ const PASSTHROUGH_MEDIA = new Set(["image/jpeg", "image/png", "image/gif", "imag
50
52
  export interface NormalizeOptions {
51
53
  /** Shift every image's starting ladder position down (413 retry tightening; 030). */
52
54
  tierBias?: number;
55
+ /** Cancels queued native decode work and stops pulling more images. */
56
+ abortSignal?: AbortSignal;
53
57
  /** Test seam: replaces the Bun.Image encode path (audit round 1, blocker 6). */
54
58
  encode?: EncodeFn;
55
59
  /** Test seam: replaces the pass-through decode validation (C-gate round 1, blocker 1). */
@@ -275,6 +279,9 @@ export const bunImageEncode: EncodeFn = async (input, spec, quality) => {
275
279
  const meta = await image.metadata();
276
280
  const w = typeof meta.width === "number" ? meta.width : 0;
277
281
  const h = typeof meta.height === "number" ? meta.height : 0;
282
+ if (w <= 0 || h <= 0 || w * h > MAX_INPUT_PIXELS) {
283
+ throw new Error("image dimensions exceed the safe decode limit");
284
+ }
278
285
  let pipeline = new Bun.Image(input);
279
286
  if (w > spec.maxEdge || h > spec.maxEdge) {
280
287
  const scale = spec.maxEdge / Math.max(w, h);
@@ -290,7 +297,14 @@ export const bunImageEncode: EncodeFn = async (input, spec, quality) => {
290
297
  * instead of riding pass-through to an Anthropic 400 (C-gate round 1, blocker 1).
291
298
  */
292
299
  export const bunImageValidate: ValidateFn = async input => {
293
- await new Bun.Image(input).resize(1, 1).jpeg({ quality: 1 }).toBuffer();
300
+ const image = new Bun.Image(input);
301
+ const meta = await image.metadata();
302
+ const w = typeof meta.width === "number" ? meta.width : 0;
303
+ const h = typeof meta.height === "number" ? meta.height : 0;
304
+ if (w <= 0 || h <= 0 || w * h > MAX_INPUT_PIXELS) {
305
+ throw new Error("image dimensions exceed the safe decode limit");
306
+ }
307
+ await image.resize(1, 1).jpeg({ quality: 1 }).toBuffer();
294
308
  };
295
309
 
296
310
  /**
@@ -32,6 +32,37 @@ import { recordedEmittedPosition, recordEmittedPosition } from "./anthropic-imag
32
32
  import { IMAGE_NORMALIZE_CONCURRENCY, MAX_INPUT_BASE64_LENGTH, MAX_INPUT_PIXELS } from "./anthropic-image-codec";
33
33
  import type { NormalizeOptions } from "./anthropic-image-codec";
34
34
 
35
+ const IMAGE_DECODE_PROCESS_CONCURRENCY = IMAGE_NORMALIZE_CONCURRENCY;
36
+ let activeImageDecodes = 0;
37
+ const imageDecodeWaiters: Array<() => void> = [];
38
+
39
+ async function enterImageDecode(signal?: AbortSignal): Promise<() => void> {
40
+ if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
41
+ let transferred = false;
42
+ if (activeImageDecodes >= IMAGE_DECODE_PROCESS_CONCURRENCY) {
43
+ await new Promise<void>((resolve, reject) => {
44
+ const admit = (): void => {
45
+ transferred = true;
46
+ signal?.removeEventListener("abort", abort);
47
+ resolve();
48
+ };
49
+ const abort = (): void => {
50
+ const index = imageDecodeWaiters.indexOf(admit);
51
+ if (index >= 0) imageDecodeWaiters.splice(index, 1);
52
+ reject(signal?.reason ?? new DOMException("Aborted", "AbortError"));
53
+ };
54
+ imageDecodeWaiters.push(admit);
55
+ signal?.addEventListener("abort", abort, { once: true });
56
+ });
57
+ }
58
+ if (!transferred) activeImageDecodes++;
59
+ return () => {
60
+ const next = imageDecodeWaiters.shift();
61
+ if (next) next();
62
+ else activeImageDecodes--;
63
+ };
64
+ }
65
+
35
66
  const UNDECODABLE_TEXT = "[image omitted: undecodable or corrupt image data]";
36
67
  const BOMB_TEXT = "[image omitted: image too large to process safely]";
37
68
  const OVERFLOW_DROP_TEXT = "[image omitted: total image payload exceeded the provider request budget; older images were dropped]";
@@ -104,8 +135,24 @@ export async function normalizeImageTargets(targets: NormalizeTarget[], options:
104
135
  const budget = options.budget ?? TOTAL_IMAGE_BASE64_BUDGET;
105
136
  const overflowAction = options.overflowAction ?? "none";
106
137
  const processLimit = options.processLimit ?? Number.POSITIVE_INFINITY;
138
+ const abortSignal = options.abortSignal;
107
139
  const n = targets.length;
108
140
 
141
+ const process = async (b64: string, pos: number, mediaType: string) => {
142
+ if (abortSignal?.aborted) throw abortSignal.reason ?? new DOMException("Aborted", "AbortError");
143
+ const leave = await enterImageDecode(abortSignal);
144
+ try {
145
+ // Re-check after admission: an abort landing between dequeue and decode-start
146
+ // must not begin decoding (an in-flight native decode cannot be interrupted).
147
+ if (abortSignal?.aborted) throw abortSignal.reason ?? new DOMException("Aborted", "AbortError");
148
+ const result = await processAt(b64, pos, mediaType, encode, validate);
149
+ if (abortSignal?.aborted) throw abortSignal.reason ?? new DOMException("Aborted", "AbortError");
150
+ return result;
151
+ } finally {
152
+ leave();
153
+ }
154
+ };
155
+
109
156
  // sourceB64/sourceMedia are the ORIGINAL input (encode source + cache identity);
110
157
  // size always reflects the bytes currently ON the wire for this target (the core is
111
158
  // the only mutator, so tracked size cannot drift from reality).
@@ -167,7 +214,7 @@ export async function normalizeImageTargets(targets: NormalizeTarget[], options:
167
214
  // age tier on a cold store, which is the pre-#4532 behaviour.
168
215
  const recorded = recordedEmittedPosition(b64, sourceMedia);
169
216
  const pos = Math.min((recorded ?? initialPosition(newestFirstIndex, 0)) + Math.max(0, bias), TERMINAL_POS);
170
- const result = await processAt(b64, pos, sourceMedia, encode, validate);
217
+ const result = await process(b64, pos, sourceMedia);
171
218
  if (result.kind === "failed") {
172
219
  target.drop(UNDECODABLE_TEXT);
173
220
  if (target.retainsBytesOnDrop) {
@@ -209,7 +256,7 @@ export async function normalizeImageTargets(targets: NormalizeTarget[], options:
209
256
  while (sum > budget) {
210
257
  const entry = entries.find((e): e is Entry => e !== null && !e.done);
211
258
  if (!entry) break; // all terminal — overflowAction below decides
212
- const result = await processAt(entry.sourceB64, entry.pos + 1, entry.sourceMedia, encode, validate);
259
+ const result = await process(entry.sourceB64, entry.pos + 1, entry.sourceMedia);
213
260
  if (result.kind === "failed") {
214
261
  entry.target.drop(UNDECODABLE_TEXT);
215
262
  if (entry.target.retainsBytesOnDrop) {
@@ -945,7 +945,10 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
945
945
  // Primary image layer: resize/re-encode to fit Anthropic limits without dropping
946
946
  // (anthropic-image-normalize.ts); the guard below remains the deterministic backstop.
947
947
  // imageTierBias > 0 = upstream-413 tightened retry (030): start every image one tier lower.
948
- await normalizeAnthropicImages(messages, { tierBias: incoming?.imageTierBias ?? 0 });
948
+ await normalizeAnthropicImages(messages, {
949
+ tierBias: incoming?.imageTierBias ?? 0,
950
+ abortSignal: incoming?.abortSignal,
951
+ });
949
952
  // Anthropic rejects many-image requests (>20 images) carrying any image over
950
953
  // 2000px per side; see anthropic-image-guard.ts for the full limit policy.
951
954
  enforceAnthropicImageLimits(messages);
@@ -1,7 +1,7 @@
1
1
  import type { AdapterEvent, OcxParsedRequest } from "../types";
2
2
  import type { TranslatorBudget } from "../lib/translator-budget";
3
3
  import type { RequestExecutionBudget } from "../lib/request-execution-budget";
4
- import type { AttemptRecoveryKind } from "../usage/log";
4
+ import type { AttemptRecoveryKind, AttemptRecoveryWithheld } from "../usage/log";
5
5
  import type { AdapterTierMetadata } from "../providers/fastwire";
6
6
 
7
7
  /** Metadata about the caller's incoming request, for auth-forwarding adapters. */
@@ -31,11 +31,34 @@ export interface IncomingMeta {
31
31
  * behind it (#4546).
32
32
  */
33
33
  sendBudget?: RequestExecutionBudget;
34
+ /**
35
+ * Physical-send observations for runTurn adapters. Without the same callback carried by
36
+ * AdapterFetchContext, an adapter-owned replay spends the shared budget but remains absent
37
+ * from the request's sendCount.
38
+ */
39
+ onPhysicalSend?: (send: { ordinal: number; recovery?: AttemptRecoveryKind }) => void;
40
+ /**
41
+ * Recovery refusals for runTurn adapters. A refused replay is not a send, so this separate
42
+ * channel explains why recovery stopped without inflating physical-send telemetry.
43
+ */
44
+ onRecoveryWithheld?: (withheld: { reason: AttemptRecoveryWithheld }) => void;
34
45
  }
35
46
 
36
47
  export interface ProviderAdapter {
37
48
  name: string;
38
49
 
50
+ /**
51
+ * This adapter reports every physical inference send through `IncomingMeta.onPhysicalSend`,
52
+ * including its first.
53
+ *
54
+ * The caller normally logs the first send before handing control over, which is correct for a
55
+ * transport whose sends it can see. An adapter that admits its own sends through the shared
56
+ * budget can have that first send refused, and a send logged before admission is a send the
57
+ * log claims and the wire never made. Setting this moves the first send's accounting to the
58
+ * boundary where it is actually dispatched.
59
+ */
60
+ reportsPhysicalSends?: boolean;
61
+
39
62
  /**
40
63
  * Convert an already-read provider HTTP error into client-safe text. This hook must be pure and
41
64
  * return fully redacted output: callers may pass untrusted provider headers and payload text.
@@ -168,6 +191,16 @@ export interface AdapterFetchContext {
168
191
  * to `sendCount` and no regression could assert a count for them (#4546).
169
192
  */
170
193
  onPhysicalSend?: (send: { ordinal: number; recovery?: AttemptRecoveryKind }) => void;
194
+ /**
195
+ * Observes a recovery this adapter was ready to make and did not, because the send budget
196
+ * refused the dispatch.
197
+ *
198
+ * Separate from `onPhysicalSend` because nothing was sent: folding it in would inflate
199
+ * `sendCount`, the one number that means "requests this proxy actually made". Without it a
200
+ * log with one send cannot distinguish "no recovery was eligible" from "one was and the
201
+ * budget withheld it", and those need opposite follow-ups (#5044).
202
+ */
203
+ onRecoveryWithheld?: (withheld: { reason: AttemptRecoveryWithheld }) => void;
171
204
  }
172
205
 
173
206
  /**
@@ -1,4 +1,4 @@
1
- import { spawn as nodeSpawn, type ChildProcess, type SpawnOptions } from "node:child_process";
1
+ import { execFileSync, spawn as nodeSpawn, type ChildProcess, type SpawnOptions } from "node:child_process";
2
2
  import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../../types";
3
3
  import { commandInvocation } from "../../lib/win-exec";
4
4
  import type { IncomingMeta } from "../base";
@@ -8,6 +8,9 @@ import { resolveCodingAgentBinary, resolveProfileByBaseUrl, type CodingAgentProv
8
8
  /** Injectable spawn for tests; production uses node:child_process. */
9
9
  export type SpawnFn = (command: string, args: readonly string[], options: SpawnOptions) => ChildProcess;
10
10
 
11
+ /** Injectable Windows process-tree terminator; production uses taskkill /T /F. */
12
+ export type KillWindowsProcessTreeFn = (pid: number) => void;
13
+
11
14
  /** Per-turn injectables: spawn/which seams for tests plus wall-clock ceilings for timeout, kill grace, and bounded reap. */
12
15
  export interface CodingAgentDeps {
13
16
  spawn?: SpawnFn;
@@ -20,6 +23,8 @@ export interface CodingAgentDeps {
20
23
  reapTimeoutMs?: number;
21
24
  /** Test seam for Windows command-shim invocation. */
22
25
  platform?: NodeJS.Platform;
26
+ /** Test seam for terminating a Windows CLI and all descendants. */
27
+ killWindowsProcessTree?: KillWindowsProcessTreeFn;
23
28
  }
24
29
 
25
30
  const DEFAULT_TIMEOUT_MS = 300_000;
@@ -27,6 +32,14 @@ const DEFAULT_KILL_GRACE_MS = 2_000;
27
32
  /** Bound captured stderr so an error message can never carry an unbounded (or secret) payload. */
28
33
  const MAX_STDERR_BYTES = 8 * 1024;
29
34
 
35
+ function killWindowsProcessTree(pid: number): void {
36
+ const taskkill = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\taskkill.exe`;
37
+ execFileSync(taskkill, ["/PID", String(pid), "/T", "/F"], {
38
+ stdio: "pipe",
39
+ windowsHide: true,
40
+ });
41
+ }
42
+
30
43
  /** Env keys a CLI needs to run; everything else is dropped so the child env is scoped and deterministic. */
31
44
  const INHERITED_ENV_KEYS = [
32
45
  "PATH", "HOME", "USERPROFILE", "LANG", "LC_ALL", "LC_CTYPE", "TMPDIR", "TEMP", "TMP",
@@ -93,6 +106,7 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
93
106
  const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
94
107
  const killGraceMs = deps.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
95
108
  const reapTimeoutMs = deps.reapTimeoutMs ?? (killGraceMs * 2 + 250);
109
+ const platform = deps.platform ?? process.platform;
96
110
 
97
111
  if (incoming.abortSignal?.aborted) {
98
112
  emit({ type: "error", message: "Coding-agent turn was aborted before start." });
@@ -140,7 +154,7 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
140
154
 
141
155
  const args = buildArgs(profile, parsed, provider);
142
156
  const env = buildEnv(profile, apiKey);
143
- const invocation = commandInvocation(binary, args, deps.platform ?? process.platform, { env });
157
+ const invocation = commandInvocation(binary, args, platform, { env });
144
158
 
145
159
  let child: ChildProcess;
146
160
  try {
@@ -197,6 +211,12 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
197
211
  const kill = (): void => {
198
212
  if (killed || child.killed) return;
199
213
  killed = true;
214
+ if (platform === "win32" && child.pid !== undefined) {
215
+ try {
216
+ (deps.killWindowsProcessTree ?? killWindowsProcessTree)(child.pid);
217
+ return;
218
+ } catch { /* fall back to terminating the direct child */ }
219
+ }
200
220
  try { child.kill("SIGTERM"); } catch { /* already gone */ }
201
221
  killTimer = setTimeout(() => {
202
222
  try { child.kill("SIGKILL"); } catch { /* already gone */ }
@@ -8,7 +8,7 @@ import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./bas
8
8
  import type { TranslatorBudget } from "../lib/translator-budget";
9
9
  import { readBoundedResponseBody } from "../lib/bounded-body";
10
10
  import { debugDroppedFrame } from "../lib/debug";
11
- import { configuredReasoningEfforts } from "../reasoning-effort";
11
+ import { configuredReasoningEfforts, modelRecordValue } from "../reasoning-effort";
12
12
  import { commandCodeReasoningEfforts, refreshCommandCodeReasoningEfforts } from "../providers/command-code-efforts";
13
13
  import { identifyRoutedModel } from "./identity";
14
14
  import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
@@ -483,13 +483,51 @@ async function fetchCommandCode(request: AdapterRequest, ctx: AdapterFetchContex
483
483
  }
484
484
  }
485
485
 
486
+ /**
487
+ * Has the operator declared their own ladders authoritative for this provider?
488
+ *
489
+ * Comparing a configured row against the shipped table cannot answer this. `providerConfigSeed`
490
+ * copies the whole table into every materialized preset, and both enrichment and routing keep a
491
+ * persisted row over the current seed, so a row written by an older release keeps its old value
492
+ * and starts LOOKING like an operator edit the moment the shipped table is corrected — at which
493
+ * point the stale row would outrank the correction and disable the rejection repair below.
494
+ * Provenance has to be declared rather than inferred, so it is: `providerConfigSeed` never
495
+ * writes this flag, which makes its presence something only a human can have caused.
496
+ */
497
+ function operatorChoseCommandCodeLadder(provider: OcxProviderConfig, canonicalId: string): boolean {
498
+ if (provider.modelReasoningEffortsAuthoritative !== true) return false;
499
+ return modelRecordValue(provider.modelReasoningEfforts, canonicalId) !== undefined;
500
+ }
501
+
502
+ /**
503
+ * Resolve the ladder this request may draw a wire effort from.
504
+ *
505
+ * The shipped table is a default, not a ceiling the operator cannot reach past. Until this
506
+ * existed the adapter read `commandCodeReasoningEfforts() ?? configuredReasoningEfforts()`, so a
507
+ * model with a row ignored configuration outright while a model without one honoured it — and
508
+ * the catalog disagreed with both, because `configuredReasoningEfforts` is what advertises the
509
+ * picker. An operator who widened a pinned row saw the wider ladder offered in Codex and then
510
+ * watched the adapter strip the rung on the way out (#5096).
511
+ *
512
+ * An operator who sets `modelReasoningEffortsAuthoritative` now resolves through the same
513
+ * function the catalog uses, so the picker and the wire agree, and sanitization, tier healing and
514
+ * learned-refusal dropping apply to it. Every other provider keeps the shipped table, including a
515
+ * value learned by a profile refresh.
516
+ */
517
+ function commandCodeEffortLadder(provider: OcxProviderConfig, canonicalId: string): readonly string[] | undefined {
518
+ if (operatorChoseCommandCodeLadder(provider, canonicalId)) {
519
+ return configuredReasoningEfforts(provider, canonicalId);
520
+ }
521
+ return commandCodeReasoningEfforts(canonicalId) ?? configuredReasoningEfforts(provider, canonicalId);
522
+ }
523
+
486
524
  function supportedCommandCodeEffort(provider: OcxProviderConfig, modelId: string, requested: string | undefined): string | undefined {
487
525
  if (!requested || requested === "none") return undefined;
488
526
  // Compatibility ids (deepseek-v4-flash / glm-5.2) must resolve to their canonical
489
527
  // Command Code id before the effort lookup, or legacy requests silently lose the
490
528
  // reasoning effort because the official table is keyed by the canonical ids.
491
529
  const canonicalId = canonicalCommandCodeModelId(modelId);
492
- const supported = commandCodeReasoningEfforts(canonicalId) ?? configuredReasoningEfforts(provider, canonicalId);
530
+ const supported = commandCodeEffortLadder(provider, canonicalId);
493
531
  if (!supported) return undefined;
494
532
  // Only remap xhigh/ultra→max for models whose official profile documents that
495
533
  // aliasing (deepseek v4, glm-5.2). Muse Spark's upstream accepts xhigh as a
@@ -501,7 +539,11 @@ function supportedCommandCodeEffort(provider: OcxProviderConfig, modelId: string
501
539
  lower === "zai-org/glm-5.2";
502
540
  if (requested === "xhigh" && !supported.includes("xhigh") && supported.includes("max")) {
503
541
  wire = "max";
504
- } else if (requested === "ultra" && needsAlias && supported.includes("max")) {
542
+ } else if (requested === "ultra" && needsAlias && !supported.includes("ultra") && supported.includes("max")) {
543
+ // The xhigh branch above already refuses to alias a rung the ladder advertises; ultra has to
544
+ // match it. No shipped row offers ultra, so this changes nothing for the built-in table — but
545
+ // an authoritative operator ladder that does offer it would otherwise advertise ultra in the
546
+ // picker and quietly send max, which is the catalog/wire disagreement this file just fixed.
505
547
  wire = "max";
506
548
  }
507
549
  return (supported as readonly string[]).includes(wire) ? wire : undefined;
@@ -576,6 +618,11 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA
576
618
  try { return (JSON.parse(request.body) as { params?: { model?: unknown } }).params?.model; } catch { return undefined; }
577
619
  })();
578
620
  if (typeof modelId !== "string") return response;
621
+ // An operator who wrote this ladder authorized the rung deliberately. Replaying the turn
622
+ // without it would answer at the provider default and hide a wrong configuration behind a
623
+ // successful-looking response, so the upstream rejection is what the caller gets. The
624
+ // downgrade below stays for the shipped table, where the rung was never the caller's idea.
625
+ if (operatorChoseCommandCodeLadder(provider, canonicalCommandCodeModelId(modelId))) return response;
579
626
  const refreshed = await refreshCommandCodeReasoningEfforts(modelId, executor);
580
627
  if (!refreshed || refreshed.includes(currentEffort)) return response;
581
628
  const retry = requestWithoutReasoningEffort(request);
@@ -60,6 +60,8 @@ const CONTEXT_500K = 500 * K;
60
60
  const CONTEXT_1M = 1_000 * K;
61
61
  /** Gemini publishes the exact power-of-two window, not a rounded 1M. */
62
62
  const CONTEXT_GEMINI = 1_048_576;
63
+ /** Meta publishes 1,048,576 for both Muse Spark 1.3 tiers (dev.meta.ai/docs/models). */
64
+ const CONTEXT_MUSE = 1_048_576;
63
65
 
64
66
  const FULL = ["low", "medium", "high", "xhigh", "max"] as const;
65
67
  const T = "thinking-then-effort" as const;
@@ -211,6 +213,15 @@ export const CURSOR_CAPABILITIES: Record<string, CursorCapability> = {
211
213
  defaultVariant: "regular",
212
214
  variants: { regular: { levels: ["low", "medium", "high"] } },
213
215
  },
216
+ // Seeded from the live GetUsableModels roster attached to #4820, which advertises six
217
+ // muse-spark-1.3 effort variants. The ladder stops at xhigh on purpose: see the matching
218
+ // effort-map entry for why Cursor advertising `-max` is not evidence that it runs.
219
+ "muse-spark-1.3": {
220
+ displayName: "Muse Spark 1.3",
221
+ window: CONTEXT_MUSE,
222
+ defaultVariant: "regular",
223
+ variants: { regular: { levels: ["minimal", "low", "medium", "high", "xhigh"] } },
224
+ },
214
225
  "kimi-k3": {
215
226
  displayName: "Kimi K3",
216
227
  window: CONTEXT_1M,
@@ -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
  /**
@@ -38,8 +38,10 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
38
38
  "claude-opus-5-fast": ["low", "medium", "high"],
39
39
  "claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"],
40
40
  "glm-5.2": ["high", "max"],
41
- // 260825 live GetUsableModels. gemini-3.6-flash is the only Cursor model exposing `minimal`;
42
- // listing it here is also what admits the suffix into CANONICAL_EFFORT_SUFFIXES below.
41
+ // 260825 live GetUsableModels. gemini-3.6-flash was the first Cursor model exposing
42
+ // `minimal`; listing a rung here is also what admits the suffix into
43
+ // CANONICAL_EFFORT_SUFFIXES below. muse-spark-1.3 now carries it too, so `minimal` no
44
+ // longer depends on this single row.
43
45
  "gemini-3.6-flash": ["minimal", "low", "medium", "high"],
44
46
  "gemini-3.7-flash": ["low", "medium", "high"],
45
47
  // 260903 preemptive: gemini-3.8-flash seeded ahead of Cursor's lineup update, the same way
@@ -91,6 +93,18 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
91
93
  "gpt-5.6-sol": ["low", "medium", "high", "xhigh", "max"],
92
94
  "gpt-5.6-terra": ["low", "medium", "high", "xhigh", "max"],
93
95
  "gpt-5.6-luna": ["low", "medium", "high", "xhigh", "max"],
96
+ // 260916 live GetUsableModels (#4820) advertises muse-spark-1.3 at minimal, low, medium,
97
+ // high, xhigh AND max. The seed stops at xhigh deliberately.
98
+ //
99
+ // Meta publishes minimal..xhigh for Muse Spark and lists no `max` at all
100
+ // (dev.meta.ai/docs/reasoning), and an independent OpenCode Zen probe of
101
+ // muse-spark-1.3-contributor-free rejected max with `unknown variant` — both already
102
+ // recorded on META_MUSE_REASONING_EFFORTS in src/providers/registry/model-seeds.ts.
103
+ // Cursor advertising a wire id is not evidence that Run accepts it; that is exactly the
104
+ // advertised-but-not-callable shape CURSOR_KNOWN_UNCALLABLE_MODEL_IDS was created for.
105
+ // Publishing the rung anyway would invent a capability on two sources' contrary evidence.
106
+ // Add `max` here once a Cursor Run at max is observed to succeed.
107
+ "muse-spark-1.3": ["minimal", "low", "medium", "high", "xhigh"],
94
108
  };
95
109
 
96
110
  /** All effort suffixes accepted when matching live Cursor model ids to configured base ids. */
@@ -13,6 +13,58 @@
13
13
  */
14
14
 
15
15
  const ECHO_MARKERS = ["[Tool Result]", "[Tool Error]", "[tool_result]"] as const;
16
+
17
+ function isEchoMarkerLine(line: string): boolean {
18
+ return (ECHO_MARKERS as readonly string[]).includes(line.replace(/^[ \t]+/, ""));
19
+ }
20
+
21
+ /**
22
+ * Drop echoed tool-result envelopes from assistant history before Cursor root replay.
23
+ *
24
+ * The prefix sniffer catches an echo that STARTS a turn, but grok-4.6 routinely writes a real
25
+ * sentence first and pastes the envelope after it. That text has already reached the client and
26
+ * is stored as assistant output, so replaying it verbatim re-primes the next turn with the very
27
+ * envelope the model is copying.
28
+ *
29
+ * Scope starts AT the marker line and runs to the next blank line, rather than to the end of
30
+ * the message. The echoed envelope has no terminator we can recognise — we build it as a marker
31
+ * line plus arbitrary result text (protobuf-request.ts), and the observed copies are not
32
+ * byte-exact, so matching against the replayed envelope is not available either. Truncating to
33
+ * the end of the message was the alternative, and it discards a genuine answer whenever the
34
+ * model resumes after the echo. A blank line is the one boundary the model reliably writes when
35
+ * it goes back to prose.
36
+ *
37
+ * The tradeoff is explicit: an echoed envelope whose pasted result itself contains a blank line
38
+ * leaves its remainder in replay. That is the safer direction to be wrong in — conversation
39
+ * remint, not this filter, is the primary defence against a poisoned conversation, and this only
40
+ * stops the transcript from feeding itself.
41
+ *
42
+ * Only whole-line markers count, so prose such as "the string [Tool Result] appeared" survives.
43
+ */
44
+ export function stripAssistantEchoedToolEnvelope(text: string): string {
45
+ if (!text || !ECHO_MARKERS.some(marker => text.includes(marker))) return text;
46
+ const newline = text.includes("\r\n") ? "\r\n" : "\n";
47
+ const lines = text.split(/\r?\n/);
48
+ const kept: string[] = [];
49
+ let dropped = false;
50
+ let index = 0;
51
+ while (index < lines.length) {
52
+ const line = lines[index] ?? "";
53
+ if (!isEchoMarkerLine(line)) {
54
+ kept.push(line);
55
+ index += 1;
56
+ continue;
57
+ }
58
+ dropped = true;
59
+ index += 1;
60
+ // The envelope body is the contiguous non-blank run after the marker. The blank line that
61
+ // ends it is left in place, so surviving prose on either side stays separated.
62
+ while (index < lines.length && (lines[index] ?? "").trim() !== "") index += 1;
63
+ }
64
+ if (!dropped) return text;
65
+ return kept.join(newline).trimEnd();
66
+ }
67
+
16
68
  const MAX_SNIFF_BYTES = 40;
17
69
  /** Mid-stream observer: max leading whitespace on a line before matching disarms. */
18
70
  const MAX_MIDSTREAM_LINE_INDENT = 128;
@@ -66,8 +118,9 @@ export interface MidstreamEchoFinding {
66
118
  * MIDDLE of an agent message — after legitimate leading text — one of them
67
119
  * carrying a whitespace-spliced call-id ("fc_x mar-y" instead of "fc_x-y").
68
120
  * Deltas at that point have already reached the client, so this observer
69
- * never throws and never withholds output: it records findings so the
70
- * adapter can emit a structured diagnostic at turn end. Only fixed marker
121
+ * never throws and never withholds output. It records findings so the adapter
122
+ * can emit a structured diagnostic and remint the conversation for the next
123
+ * turn at turn end. Only fixed marker
71
124
  * enums, numeric offsets, and corruption booleans are retained — never
72
125
  * content bytes.
73
126
  */