@bitkyc08/opencodex 2.10.0 → 2.10.1

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 (274) hide show
  1. package/AGENTS_INSTALL.md +77 -0
  2. package/README.md +4 -10
  3. package/bin/ocx.mjs +71 -18
  4. package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
  5. package/gui/dist/assets/index-ChZQsmBY.js +70 -0
  6. package/gui/dist/index.html +2 -2
  7. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  8. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  9. package/gui/dist/provider-icons/claude-color.svg +1 -1
  10. package/gui/dist/provider-icons/cline-color.svg +16 -0
  11. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  12. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  13. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  14. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  15. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  16. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  17. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  18. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  19. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  20. package/gui/dist/provider-icons/grok.svg +1 -1
  21. package/gui/dist/provider-icons/groq-color.svg +1 -1
  22. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  23. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  24. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  25. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  26. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  27. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  28. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  29. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  30. package/gui/dist/provider-icons/openai.svg +1 -1
  31. package/gui/dist/provider-icons/opencode.svg +2 -1
  32. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  33. package/gui/dist/provider-icons/pi.svg +2 -2
  34. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  35. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  36. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  37. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  38. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  39. package/package.json +8 -4
  40. package/src/adapters/anthropic.ts +208 -14
  41. package/src/adapters/base.ts +16 -5
  42. package/src/adapters/cursor/effort-map.ts +3 -2
  43. package/src/adapters/cursor/framing.ts +39 -0
  44. package/src/adapters/cursor/live-transport.ts +105 -95
  45. package/src/adapters/cursor/native-exec.ts +32 -6
  46. package/src/adapters/cursor/protobuf-request.ts +20 -15
  47. package/src/adapters/cursor/request-builder.ts +21 -7
  48. package/src/adapters/cursor/types.ts +7 -0
  49. package/src/adapters/google-antigravity-replay.ts +237 -21
  50. package/src/adapters/google-truncation.ts +11 -0
  51. package/src/adapters/google.ts +50 -9
  52. package/src/adapters/identity.ts +39 -6
  53. package/src/adapters/kiro-errors.ts +11 -0
  54. package/src/adapters/kiro-events.ts +19 -1
  55. package/src/adapters/kiro-thinking.ts +10 -2
  56. package/src/adapters/kiro-tools.ts +10 -1
  57. package/src/adapters/kiro.ts +37 -11
  58. package/src/adapters/openai-chat.ts +284 -83
  59. package/src/adapters/openai-responses.ts +182 -24
  60. package/src/bridge.ts +177 -7
  61. package/src/chat/outbound.ts +78 -23
  62. package/src/claude/agents-inject.ts +27 -5
  63. package/src/claude/inbound.ts +11 -1
  64. package/src/claude/model-info.ts +13 -10
  65. package/src/claude/outbound.ts +17 -0
  66. package/src/cli/account-api.ts +24 -0
  67. package/src/cli/account-auth.ts +31 -6
  68. package/src/cli/account-main.ts +317 -0
  69. package/src/cli/account.ts +5 -0
  70. package/src/cli/claude.ts +2 -1
  71. package/src/cli/doctor.ts +93 -22
  72. package/src/cli/export-command.ts +26 -12
  73. package/src/cli/help.ts +8 -6
  74. package/src/cli/index.ts +56 -22
  75. package/src/cli/integrations.ts +84 -1
  76. package/src/cli/observe.ts +54 -1
  77. package/src/cli/opencode.ts +2 -1
  78. package/src/cli/provider-runtime.ts +18 -1
  79. package/src/cli/route-policy.ts +92 -0
  80. package/src/cli/runtime-api.ts +6 -3
  81. package/src/cli/star-prompt.ts +71 -15
  82. package/src/cli/status.ts +1 -1
  83. package/src/cli/v2.ts +36 -9
  84. package/src/clients/config-export.ts +687 -10
  85. package/src/codex/account-lifecycle.ts +30 -5
  86. package/src/codex/account-usability.ts +22 -2
  87. package/src/codex/admission.ts +255 -0
  88. package/src/codex/auth-api.ts +427 -140
  89. package/src/codex/auth-context.ts +155 -30
  90. package/src/codex/autostart-health.ts +8 -1
  91. package/src/codex/catalog/account-models.ts +62 -0
  92. package/src/codex/catalog/aggregation.ts +14 -1
  93. package/src/codex/catalog/bundled.ts +282 -32
  94. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  95. package/src/codex/catalog/metadata.ts +51 -6
  96. package/src/codex/catalog/parsing.ts +6 -3
  97. package/src/codex/catalog/provider-fetch.ts +576 -41
  98. package/src/codex/catalog/sync.ts +505 -66
  99. package/src/codex/catalog-admission.ts +197 -0
  100. package/src/codex/catalog-write-serialization.ts +241 -0
  101. package/src/codex/catalog.ts +2 -1
  102. package/src/codex/codex-write-lock.ts +372 -0
  103. package/src/codex/convergence-types.ts +593 -0
  104. package/src/codex/convergence.ts +441 -0
  105. package/src/codex/desired-state.ts +177 -0
  106. package/src/codex/features.ts +52 -8
  107. package/src/codex/generation.ts +202 -0
  108. package/src/codex/history-job.ts +257 -0
  109. package/src/codex/history-lock.ts +241 -0
  110. package/src/codex/history-migration-guardian.ts +18 -5
  111. package/src/codex/history-provider.ts +9 -2
  112. package/src/codex/history-transition.ts +105 -0
  113. package/src/codex/history-worker.ts +176 -0
  114. package/src/codex/inject-coordination.ts +245 -0
  115. package/src/codex/inject.ts +605 -124
  116. package/src/codex/integration-record.ts +266 -0
  117. package/src/codex/internal/catalog-writer.ts +203 -0
  118. package/src/codex/internal/history-writer.ts +80 -0
  119. package/src/codex/journal.ts +10 -1
  120. package/src/codex/main-account-cache.ts +24 -0
  121. package/src/codex/management-convergence.ts +114 -0
  122. package/src/codex/native-main-admission.ts +47 -0
  123. package/src/codex/native-main-auth-temp.ts +187 -0
  124. package/src/codex/native-main-claim.ts +167 -0
  125. package/src/codex/native-main-lock-file.ts +158 -0
  126. package/src/codex/native-main-owner.ts +315 -0
  127. package/src/codex/native-profile-api.ts +247 -0
  128. package/src/codex/native-profile-manager.ts +1512 -0
  129. package/src/codex/native-profile-processes.ts +121 -0
  130. package/src/codex/native-profile-recovery.ts +99 -0
  131. package/src/codex/native-profile-stage-store.ts +387 -0
  132. package/src/codex/native-profile-startup.ts +340 -0
  133. package/src/codex/native-profile-store.ts +855 -0
  134. package/src/codex/native-profile-types.ts +120 -0
  135. package/src/codex/native-residue.ts +557 -0
  136. package/src/codex/project-config-warnings.ts +18 -4
  137. package/src/codex/prompt-journal.ts +311 -0
  138. package/src/codex/prompt-layers.ts +967 -0
  139. package/src/codex/prompt-lock.ts +143 -0
  140. package/src/codex/quota-rejection.ts +224 -0
  141. package/src/codex/quota.ts +86 -3
  142. package/src/codex/routing.ts +299 -62
  143. package/src/codex/runtime.ts +159 -38
  144. package/src/codex/shim.ts +39 -13
  145. package/src/codex/subagent-model-fallback.ts +73 -12
  146. package/src/codex/transition-state.ts +604 -0
  147. package/src/codex/upstream-host-health.ts +70 -0
  148. package/src/codex/user-identity.ts +266 -0
  149. package/src/codex/write-coordination.ts +114 -0
  150. package/src/config.ts +562 -26
  151. package/src/generated/jawcode-model-metadata.ts +2 -2
  152. package/src/grok/inject.ts +15 -4
  153. package/src/grok/inspect.ts +45 -0
  154. package/src/images/loop.ts +113 -20
  155. package/src/integrations/config-io.ts +151 -0
  156. package/src/integrations/journal.ts +315 -0
  157. package/src/integrations/merge.ts +135 -0
  158. package/src/integrations/native/ownership-preflight.ts +165 -0
  159. package/src/integrations/ownership.ts +111 -0
  160. package/src/integrations/registry.ts +101 -0
  161. package/src/integrations/serialize.ts +235 -0
  162. package/src/integrations/state.ts +290 -0
  163. package/src/integrations/store.ts +103 -0
  164. package/src/integrations/writer.ts +492 -0
  165. package/src/lib/bounded-body.ts +46 -8
  166. package/src/lib/bun-runtime.ts +110 -1
  167. package/src/lib/bun-stream-caps.ts +2 -1
  168. package/src/lib/redact.ts +407 -2
  169. package/src/lib/shadow-call.ts +24 -0
  170. package/src/lib/translator-budget.ts +10 -0
  171. package/src/lib/upstream-reachability.ts +91 -0
  172. package/src/lib/upstream-retry.ts +154 -2
  173. package/src/lib/windows-secret-acl.ts +212 -11
  174. package/src/lib/winsw.ts +9 -3
  175. package/src/oauth/index.ts +61 -3
  176. package/src/oauth/key-providers.ts +4 -0
  177. package/src/oauth/kiro.ts +50 -6
  178. package/src/oauth/store.ts +31 -6
  179. package/src/oauth/token-guardian.ts +9 -3
  180. package/src/providers/codex-capacity.ts +288 -0
  181. package/src/providers/derive.ts +33 -1
  182. package/src/providers/free-directory.ts +3 -1
  183. package/src/providers/key-failover.ts +71 -3
  184. package/src/providers/openai-sidecar.ts +64 -4
  185. package/src/providers/openai-virtual-models.ts +1 -0
  186. package/src/providers/quota.ts +334 -26
  187. package/src/providers/registry.ts +284 -16
  188. package/src/providers/xai-transport.ts +11 -4
  189. package/src/responses/compaction.ts +8 -1
  190. package/src/responses/hosted-tool-policy.ts +9 -0
  191. package/src/responses/parser.ts +44 -2
  192. package/src/responses/reasoning-envelope.ts +9 -1
  193. package/src/responses/reasoning-replay-cache.ts +105 -0
  194. package/src/responses/spill-store.ts +45 -8
  195. package/src/responses/state.ts +161 -17
  196. package/src/router.ts +302 -16
  197. package/src/routing/analytics.ts +377 -0
  198. package/src/routing/capability.ts +204 -0
  199. package/src/routing/cost.ts +77 -0
  200. package/src/routing/evaluator.ts +444 -0
  201. package/src/routing/health.ts +401 -0
  202. package/src/routing/history/cursor.ts +43 -0
  203. package/src/routing/history/indexer.ts +590 -0
  204. package/src/routing/history/schema.ts +72 -0
  205. package/src/routing/profile.ts +423 -0
  206. package/src/routing/quota.ts +124 -0
  207. package/src/routing/request-evidence.ts +45 -0
  208. package/src/routing/trace.ts +686 -0
  209. package/src/server/auth-cors.ts +46 -6
  210. package/src/server/chat-completions.ts +28 -13
  211. package/src/server/claude-messages.ts +23 -15
  212. package/src/server/gui-static.ts +39 -10
  213. package/src/server/images.ts +10 -1
  214. package/src/server/index.ts +238 -52
  215. package/src/server/lifecycle.ts +228 -9
  216. package/src/server/live.ts +14 -3
  217. package/src/server/management/agent-settings-routes.ts +64 -14
  218. package/src/server/management/combo-routes.ts +5 -5
  219. package/src/server/management/config-routes.ts +1 -1
  220. package/src/server/management/context.ts +42 -2
  221. package/src/server/management/integration-routes.ts +538 -0
  222. package/src/server/management/logs-usage-routes.ts +1 -1
  223. package/src/server/management/model-routes.ts +32 -113
  224. package/src/server/management/model-rows.ts +117 -0
  225. package/src/server/management/native-integration-routes.ts +587 -0
  226. package/src/server/management/oauth-account-routes.ts +1 -1
  227. package/src/server/management/provider-routes.ts +218 -117
  228. package/src/server/management/request-history-routes.ts +191 -0
  229. package/src/server/management/routing-analytics-routes.ts +74 -0
  230. package/src/server/management/routing-profile-routes.ts +177 -0
  231. package/src/server/management/shared.ts +2 -2
  232. package/src/server/management/sidebar-routes.ts +47 -31
  233. package/src/server/management/sync-response.ts +69 -0
  234. package/src/server/management/system-restart.ts +276 -24
  235. package/src/server/management/system-routes.ts +4 -0
  236. package/src/server/management-api.ts +84 -9
  237. package/src/server/management-auth.ts +43 -5
  238. package/src/server/relay-eager.ts +82 -42
  239. package/src/server/relay.ts +120 -6
  240. package/src/server/request-log.ts +26 -6
  241. package/src/server/responses/collaboration.ts +63 -8
  242. package/src/server/responses/compact.ts +272 -41
  243. package/src/server/responses/core.ts +730 -132
  244. package/src/server/responses/fetch-helpers.ts +15 -1
  245. package/src/server/responses-item-id-repair.ts +32 -3
  246. package/src/server/responses-json-events.ts +52 -0
  247. package/src/server/responses-snapshot-repair.ts +621 -0
  248. package/src/server/search.ts +51 -6
  249. package/src/server/sse-payload-rewrite.ts +89 -12
  250. package/src/server/startup-health-cache.ts +7 -1
  251. package/src/server/ws-bridge.ts +11 -17
  252. package/src/service-manager-probe.ts +297 -0
  253. package/src/service.ts +222 -32
  254. package/src/tray/windows-tray.ps1 +9 -0
  255. package/src/tray/windows.ts +15 -7
  256. package/src/types.ts +194 -14
  257. package/src/update/index.ts +13 -13
  258. package/src/update/job.ts +24 -21
  259. package/src/update/notify.ts +7 -3
  260. package/src/usage/cost.ts +0 -0
  261. package/src/usage/expected-prices.ts +129 -10
  262. package/src/usage/log.ts +50 -15
  263. package/src/usage/summary.ts +4 -4
  264. package/src/vision/index.ts +6 -1
  265. package/src/web-search/loop.ts +161 -34
  266. package/gui/dist/assets/index-OY43ubAq.css +0 -1
  267. package/gui/dist/assets/index-YwNnKZcL.js +0 -67
  268. package/gui/dist/provider-icons/antigravity.svg +0 -1
  269. package/gui/dist/provider-icons/claude.svg +0 -1
  270. package/gui/dist/provider-icons/copilot.svg +0 -1
  271. package/gui/dist/provider-icons/cursor.svg +0 -2
  272. package/gui/dist/provider-icons/gemini.svg +0 -1
  273. package/gui/dist/provider-icons/grok-color.svg +0 -1
  274. package/gui/dist/provider-icons/kiro.svg +0 -14
@@ -1,7 +1,7 @@
1
1
  import http2 from "node:http2";
2
2
  import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
3
3
  import { namespacedToolName, type OcxProviderConfig, type OcxUsage } from "../../types";
4
- import { CONNECT_FLAG_END_STREAM, decodeAvailableConnectFrames, encodeConnectFrame } from "./framing";
4
+ import { CONNECT_FLAG_END_STREAM, ConnectFrameError, consumeConnectFrames, encodeConnectFrame } from "./framing";
5
5
  import {
6
6
  CURSOR_MAX_EFFECTIVE_CONNECT_PAYLOAD_BYTES,
7
7
  CURSOR_MAX_CONNECT_FRAME_BYTES,
@@ -787,6 +787,7 @@ class LiveCursorTransport implements CursorTransport {
787
787
  clearTimer: () => this.clearFirstFrameTimer(),
788
788
  });
789
789
  const failAndClear = (error: Error) => {
790
+ releaseBacklogLease();
790
791
  if (this.expectedClose) {
791
792
  // We already emitted a terminal `done` and cancelled the run (client-tool suspension). The
792
793
  // RST_STREAM CANCEL surfaces here as a stream error/abort; it is expected, not a failure.
@@ -822,28 +823,53 @@ class LiveCursorTransport implements CursorTransport {
822
823
  // close() waits for in-flight frames; a dead socket can ignore it — force-destroy shortly
823
824
  // after so a stalled TLS session cannot linger past the timeout.
824
825
  armTimeoutDestroyFallback(stream, session, this.input.timeoutDestroyGraceMs ?? CURSOR_TIMEOUT_DESTROY_GRACE_MS);
826
+ releaseBacklogLease();
825
827
  settler.settleFail(new Error("Cursor transport timed out before first response"));
826
828
  }, this.input.firstFrameTimeoutMs ?? CURSOR_FIRST_FRAME_TIMEOUT_MS);
827
829
 
828
- let pending: Uint8Array<ArrayBufferLike> = new Uint8Array();
829
- let frameWork: Promise<void> = Promise.resolve();
830
- const reservePayloadCopy = (bytes: number) => {
831
- if (this.transportBufferedBytes + bytes > CURSOR_TRANSPORT_MAX_BUFFERED_BYTES) {
832
- throw new TranslatorBudgetExceededError("cursor_transport", CURSOR_TRANSPORT_MAX_BUFFERED_BYTES);
830
+ // Raw Connect backlog with a parse cursor: appends copy only the incoming
831
+ // chunk (amortized capacity growth), the consumed prefix is reclaimed
832
+ // lazily, and the RAW used length (headers included) is what the 32 MiB
833
+ // transport cap bounds payload-only accounting let tiny-frame/header
834
+ // floods slip through.
835
+ let backlog = new Uint8Array();
836
+ let backlogStart = 0;
837
+ let backlogEnd = 0;
838
+ const BACKLOG_COMPACT_MIN_SAVINGS = 64 * 1024;
839
+ const appendBacklog = (chunk: Uint8Array): void => {
840
+ const used = backlogEnd - backlogStart;
841
+ let start = backlogStart;
842
+ let end = backlogEnd;
843
+ // Reclaim the consumed prefix when it is large or needed for capacity.
844
+ if (start > 0 && (start >= BACKLOG_COMPACT_MIN_SAVINGS || end + chunk.byteLength > backlog.byteLength)) {
845
+ backlog = backlog.slice(start, end);
846
+ start = 0;
847
+ end = used;
848
+ }
849
+ if (end + chunk.byteLength > backlog.byteLength) {
850
+ const capacity = Math.max(8192, backlog.byteLength * 2, end + chunk.byteLength);
851
+ const next = new Uint8Array(Math.min(CURSOR_TRANSPORT_MAX_BUFFERED_BYTES, capacity));
852
+ next.set(backlog.subarray(start, end), 0);
853
+ backlog = next;
833
854
  }
834
- const reservation = this.translatorBudget.reserveTransient(bytes, { kind: "cursor_transport" });
835
- this.transportBufferedBytes += bytes;
836
- this.updateTransportFlowControl();
837
- return {
838
- commitRetained: () => reservation.commitRetained(),
839
- release: () => {
840
- reservation.release();
841
- this.transportBufferedBytes = Math.max(0, this.transportBufferedBytes - bytes);
842
- this.updateTransportFlowControl();
843
- },
844
- };
855
+ backlog.set(chunk, end);
856
+ backlogStart = start;
857
+ backlogEnd = end + chunk.byteLength;
858
+ };
859
+ let frameWork: Promise<void> = Promise.resolve();
860
+ // Idempotent terminal owner for the backlog lease: every settle/close path
861
+ // must leave the raw charge at zero instead of relying on budget disposal.
862
+ let backlogLeaseReleased = false;
863
+ const releaseBacklogLease = () => {
864
+ if (backlogLeaseReleased) return;
865
+ backlogLeaseReleased = true;
866
+ const leftover = backlogEnd - backlogStart;
867
+ if (leftover > 0) this.releaseTransportBytes(leftover);
868
+ backlog = new Uint8Array();
869
+ backlogStart = 0;
870
+ backlogEnd = 0;
845
871
  };
846
- const handleFrame = async (frame: ReturnType<typeof decodeAvailableConnectFrames>["frames"][number]) => {
872
+ const handleFrame = async (frame: ReturnType<typeof consumeConnectFrames>["frames"][number]) => {
847
873
  this.framesReceived++;
848
874
  if ((frame.flags & CONNECT_FLAG_END_STREAM) === CONNECT_FLAG_END_STREAM) {
849
875
  const endError = parseConnectEndStreamError(frame.payload);
@@ -861,22 +887,25 @@ class LiveCursorTransport implements CursorTransport {
861
887
  };
862
888
  const drainPendingFrames = () => {
863
889
  const availableSlots = CURSOR_MAX_PENDING_FRAMES - this.pendingTransportFrames;
864
- if (availableSlots <= 0 || pending.byteLength === 0) {
890
+ const used = backlogEnd - backlogStart;
891
+ if (availableSlots <= 0 || used === 0) {
865
892
  this.updateTransportFlowControl();
866
893
  return;
867
894
  }
868
- const previousPayloadBytes = connectBufferedPayloadBytes(pending);
869
- // The decoder materializes payload and residual copies. Keep the aggregate pending owner
870
- // charged until every replacement has been admitted and committed; a failed admission must
871
- // leave that predecessor lease intact for deterministic cleanup.
872
- const decoded = decodeAvailableConnectFrames(
873
- pending,
895
+ // Cursor decode, zero-copy: frame payloads are views into the backlog, so
896
+ // the charge TRANSFERS only consumed header bytes leave the counter
897
+ // (payload bytes stay charged and are released when each frame's work
898
+ // finishes). An exact 16 MiB payload therefore peaks at 16 MiB + 5, not
899
+ // at double its size.
900
+ const decoded = consumeConnectFrames(
901
+ backlog.subarray(backlogStart, backlogEnd),
874
902
  CURSOR_MAX_EFFECTIVE_CONNECT_PAYLOAD_BYTES,
875
903
  availableSlots,
876
- reservePayloadCopy,
877
904
  );
878
- pending = decoded.remainder;
879
- this.releaseTransportBytes(previousPayloadBytes);
905
+ if (decoded.frames.length > 0) {
906
+ backlogStart += decoded.consumedBytes;
907
+ this.releaseTransportBytes(decoded.consumedBytes - decoded.frames.reduce((n, frame) => n + frame.payload.byteLength, 0));
908
+ }
880
909
  for (const frame of decoded.frames) {
881
910
  this.pendingTransportFrames += 1;
882
911
  this.updateTransportFlowControl();
@@ -893,32 +922,31 @@ class LiveCursorTransport implements CursorTransport {
893
922
  };
894
923
  this.stream.on("data", chunk => {
895
924
  this.clearFirstFrameTimer();
925
+ // Once the turn has settled, late network bytes must never be charged —
926
+ // the backlog lease is already released and nobody would own these.
927
+ if (settler.settled()) return;
896
928
  if (!this.firstFrameLogged) {
897
929
  this.firstFrameLogged = true;
898
930
  this.firstFrameAt = Date.now();
899
931
  debugProviderDiagnostic("cursor", "first-frame", { latencyMs: this.firstFrameAt - this.turnStartedAt });
900
932
  }
901
933
  const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
902
- let incomingPayloadBytes = 0;
903
- let incomingCharged = false;
904
- let replacement: ReturnType<typeof reservePayloadCopy> | undefined;
934
+ let charged = false;
935
+ let appended = false;
905
936
  try {
906
- const previousPendingPayloadBytes = connectBufferedPayloadBytes(pending);
907
- const nextPendingPayloadBytes = connectBufferedPayloadBytesAcross(pending, bytes);
908
- incomingPayloadBytes = Math.max(0, nextPendingPayloadBytes - previousPendingPayloadBytes);
909
- this.reserveTransportBytes(incomingPayloadBytes);
910
- incomingCharged = true;
911
- replacement = reservePayloadCopy(nextPendingPayloadBytes);
912
- const nextPending = concatBytes(pending, bytes);
913
- pending = nextPending;
914
- replacement.commitRetained();
915
- replacement = undefined;
916
- this.releaseTransportBytes(previousPendingPayloadBytes + incomingPayloadBytes);
917
- incomingCharged = false;
937
+ // RAW chunk bytes (headers included) join the backlog charge; consumed
938
+ // bytes leave it at drain. No whole-backlog replacement copy anymore.
939
+ this.reserveTransportBytes(bytes.byteLength);
940
+ charged = true;
941
+ appendBacklog(bytes);
942
+ appended = true;
918
943
  drainPendingFrames();
919
944
  } catch (err) {
920
- replacement?.release();
921
- if (incomingCharged) this.releaseTransportBytes(incomingPayloadBytes);
945
+ // Release ONLY when the reservation succeeded but the append never
946
+ // happened. A failed reservation charged nothing — releasing here
947
+ // would debit unrelated existing ownership; an appended chunk is owned
948
+ // by the terminal backlog cleanup.
949
+ if (charged && !appended) this.releaseTransportBytes(bytes.byteLength);
922
950
  failAndClear(err instanceof Error ? err : new Error(String(err)));
923
951
  }
924
952
  });
@@ -954,15 +982,38 @@ class LiveCursorTransport implements CursorTransport {
954
982
  expectedClose: this.expectedClose,
955
983
  elapsedMs: Date.now() - this.turnStartedAt,
956
984
  });
957
- // A zero-frame end without an expected close is an unexpected EOF (peer dropped the
958
- // connection before any response frame) surfacing it as success would silently
959
- // swallow the turn (WP4 review blocker 1). With frames, the protobuf event state
960
- // owns terminal semantics as before.
961
- if (this.framesReceived === 0 && !this.expectedClose) {
962
- settler.settleFail(new Error("Cursor stream ended before any response frame (unexpected EOF)"));
963
- return;
964
- }
965
- settler.settleFinish();
985
+ // Settle only after queued frame work drains to quiescence (the chain can
986
+ // extend itself while draining), then classify the terminal state:
987
+ // a trailing incomplete frame is a typed failure, and a zero-frame,
988
+ // zero-byte end without an expected close stays the existing unexpected
989
+ // EOF both beat the old silent success.
990
+ void (async () => {
991
+ let previous: Promise<void>;
992
+ do {
993
+ previous = frameWork;
994
+ await previous;
995
+ } while (previous !== frameWork);
996
+ })().then(() => {
997
+ if (settler.settled()) return;
998
+ const leftover = backlogEnd - backlogStart;
999
+ if (leftover > 0 && !this.expectedClose) {
1000
+ releaseBacklogLease();
1001
+ settler.settleFail(new ConnectFrameError(
1002
+ "frame_incomplete",
1003
+ `Cursor Connect stream ended with ${leftover} unconsumed bytes (incomplete frame)`,
1004
+ ));
1005
+ return;
1006
+ }
1007
+ if (this.framesReceived === 0 && !this.expectedClose) {
1008
+ releaseBacklogLease();
1009
+ settler.settleFail(new Error("Cursor stream ended before any response frame (unexpected EOF)"));
1010
+ return;
1011
+ }
1012
+ releaseBacklogLease();
1013
+ settler.settleFinish();
1014
+ }, (err) => {
1015
+ failAndClear(err instanceof Error ? err : new Error(String(err)));
1016
+ });
966
1017
  });
967
1018
 
968
1019
  signal?.addEventListener("abort", () => {
@@ -1140,47 +1191,6 @@ export function isClientToolFrame(message: AgentServerMessage): boolean {
1140
1191
  }
1141
1192
  }
1142
1193
 
1143
- function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
1144
- const out = new Uint8Array(a.length + b.length);
1145
- out.set(a);
1146
- out.set(b, a.length);
1147
- return out;
1148
- }
1149
-
1150
- function connectBufferedPayloadBytes(input: Uint8Array): number {
1151
- let offset = 0;
1152
- let payloadBytes = 0;
1153
- while (input.byteLength - offset >= 5) {
1154
- const length = new DataView(input.buffer, input.byteOffset + offset, input.byteLength - offset).getUint32(1, false);
1155
- const available = Math.min(length, input.byteLength - offset - 5);
1156
- payloadBytes += available;
1157
- if (available < length) break;
1158
- offset += 5 + length;
1159
- }
1160
- return payloadBytes;
1161
- }
1162
-
1163
- /** Payload-byte count for a virtual concatenation, without allocating the concatenated buffer. */
1164
- function connectBufferedPayloadBytesAcross(first: Uint8Array, second: Uint8Array): number {
1165
- const totalBytes = first.byteLength + second.byteLength;
1166
- const byteAt = (index: number): number => index < first.byteLength
1167
- ? first[index]!
1168
- : second[index - first.byteLength]!;
1169
- let offset = 0;
1170
- let payloadBytes = 0;
1171
- while (totalBytes - offset >= 5) {
1172
- const length = (byteAt(offset + 1) * 0x1000000)
1173
- + (byteAt(offset + 2) << 16)
1174
- + (byteAt(offset + 3) << 8)
1175
- + byteAt(offset + 4);
1176
- const available = Math.min(length, totalBytes - offset - 5);
1177
- payloadBytes += available;
1178
- if (available < length) break;
1179
- offset += 5 + length;
1180
- }
1181
- return payloadBytes;
1182
- }
1183
-
1184
1194
  /** Host-only label for Cursor transport diagnostics — never leaks path/query/credentials. */
1185
1195
  function cursorHostLabel(baseUrl: string): string {
1186
1196
  try {
@@ -128,6 +128,8 @@ const blobs = new Map<string, CursorBlobEntry>();
128
128
  const blobRequestScopes = new Map<CursorBlobRequestScopeToken, CursorBlobRequestScopeState>();
129
129
  let blobLimits = { ...DEFAULT_BLOB_LIMITS };
130
130
  let blobBytes = 0;
131
+ /** Retained key-string bytes (separate from the payload cap — see key()). */
132
+ let blobKeyBytes = 0;
131
133
  let blobLocalBytes = 0;
132
134
  let blobPinnedBytes = 0;
133
135
  let blobEvictableBytes = 0;
@@ -152,13 +154,16 @@ function recomputeBlobClassAccounting(): void {
152
154
  let pinnedBytes = 0;
153
155
  let evictableBytes = 0;
154
156
  let oldestAt: number | null = null;
155
- for (const entry of blobs.values()) {
157
+ for (const [k, entry] of blobs) {
156
158
  const requestPinned = entry.requestPins.size > 0;
157
159
  const provenancePinned = entry.provenance === "remote-setBlobArgs" && !isExpired(entry, now);
158
160
  if (entry.provenance === "local-regenerated") localBytes += entry.sizeBytes;
159
- if (requestPinned || provenancePinned) pinnedBytes += entry.sizeBytes;
161
+ // Key strings classify WITH their entry: a zero-payload blob must stay
162
+ // evictable/pinned exactly as its payload would be, or the budget cannot
163
+ // select it even though the total snapshot counts its key.
164
+ if (requestPinned || provenancePinned) pinnedBytes += entry.sizeBytes + k.length;
160
165
  if (!requestPinned && (entry.provenance === "local-regenerated" || isExpired(entry, now))) {
161
- evictableBytes += entry.sizeBytes;
166
+ evictableBytes += entry.sizeBytes + k.length;
162
167
  oldestAt = oldestAt === null ? entry.storedAt : Math.min(oldestAt, entry.storedAt);
163
168
  }
164
169
  }
@@ -196,9 +201,12 @@ function deleteBlob(k: string, recompute = true): number {
196
201
  if (!entry) return 0;
197
202
  blobs.delete(k);
198
203
  blobBytes -= entry.sizeBytes;
204
+ blobKeyBytes -= k.length;
199
205
  for (const scope of entry.requestPins) blobRequestScopes.get(scope)?.keys.delete(k);
200
206
  if (recompute) recomputeBlobClassAccounting();
201
- return entry.sizeBytes;
207
+ // Full logical release (payload + key): the retained-store snapshot counts
208
+ // both, so budget enforcement must see both leave.
209
+ return entry.sizeBytes + k.length;
202
210
  }
203
211
 
204
212
  function releaseHydratedBlob(k: string, requestScope?: CursorBlobRequestScopeToken): void {
@@ -313,6 +321,7 @@ function setBlob(
313
321
  if (blobs.has(k)) deleteBlob(k, false);
314
322
  blobs.set(k, entry);
315
323
  blobBytes += entry.sizeBytes;
324
+ blobKeyBytes += k.length;
316
325
  for (const scope of entry.requestPins) blobRequestScopes.get(scope)?.keys.add(k);
317
326
  reconcileBlobClassAccountingAndEnforce();
318
327
  return { admitted: true, replaced: existing !== undefined };
@@ -328,8 +337,20 @@ function getBlob(k: string): Uint8Array | undefined {
328
337
  return entry.data;
329
338
  }
330
339
 
340
+ /**
341
+ * Raw blob IDs up to this size keep their hex passthrough (every ID the live
342
+ * protocol carries is a 32-byte digest). Anything larger maps to a fixed-size
343
+ * SHA-256 hex of the raw bytes — the derivation is symmetric across
344
+ * setBlobArgs/getBlobArgs, so the round-trip still works, but a hostile or
345
+ * malformed multi-MiB ID can never become an unbounded hex Map key.
346
+ * The `h:`/`d:` prefix domain-separates the two namespaces: a digested ID's
347
+ * key can never collide with a raw 32-byte ID that happens to BE that digest.
348
+ */
349
+ const MAX_BLOB_ID_PASSTHROUGH_BYTES = 64;
350
+
331
351
  function key(bytes: Uint8Array): string {
332
- return Buffer.from(bytes).toString("hex");
352
+ if (bytes.byteLength <= MAX_BLOB_ID_PASSTHROUGH_BYTES) return `h:${Buffer.from(bytes).toString("hex")}`;
353
+ return `d:${createHash("sha256").update(bytes).digest("hex")}`;
333
354
  }
334
355
 
335
356
  /**
@@ -382,6 +403,7 @@ export function storeCursorBlob(data: Uint8Array, requestScope?: CursorBlobReque
382
403
  export interface CursorBlobMetrics {
383
404
  count: number;
384
405
  totalBytes: number;
406
+ keyBytes: number;
385
407
  localBytes: number;
386
408
  pinnedBytes: number;
387
409
  rejectedEntryTooLarge: number;
@@ -393,6 +415,7 @@ export function cursorBlobMetrics(): CursorBlobMetrics {
393
415
  return {
394
416
  count: blobs.size,
395
417
  totalBytes: blobBytes,
418
+ keyBytes: blobKeyBytes,
396
419
  localBytes: blobLocalBytes,
397
420
  pinnedBytes: blobPinnedBytes,
398
421
  rejectedEntryTooLarge,
@@ -410,7 +433,9 @@ export function cursorBlobRetainedStoreSnapshot(): {
410
433
  } {
411
434
  return {
412
435
  count: blobs.size,
413
- bytes: blobBytes,
436
+ // Payload + retained key strings: the framework must see everything the
437
+ // store retains. The 64 MiB admission cap stays payload-only by design.
438
+ bytes: blobBytes + blobKeyBytes,
414
439
  evictableBytes: blobEvictableBytes,
415
440
  pinnedBytes: blobPinnedBytes,
416
441
  oldestAt: blobOldestEvictableAt,
@@ -440,6 +465,7 @@ export function resetCursorBlobStateForTests(): void {
440
465
  blobs.clear();
441
466
  blobRequestScopes.clear();
442
467
  blobBytes = 0;
468
+ blobKeyBytes = 0;
443
469
  rejectedEntryTooLarge = 0;
444
470
  rejectedPinnedSaturation = 0;
445
471
  recomputeBlobClassAccounting();
@@ -621,6 +621,11 @@ function buildPreparedCursorRunRequest(
621
621
  tools: request.tools?.length ?? 0,
622
622
  });
623
623
 
624
+ const requestedModelParameters = [
625
+ ...(request.requestedModelParameters ?? []),
626
+ ...(request.routingLevel ? [{ id: CURSOR_ROUTING_LEVEL_PARAMETER_ID, value: request.routingLevel }] : []),
627
+ ];
628
+ const hasExplicitModelParameters = (request.requestedModelParameters?.length ?? 0) > 0;
624
629
  const runRequest = create(AgentRunRequestSchema, {
625
630
  conversationId: request.conversationId,
626
631
  conversationState: create(ConversationStateStructureSchema, {
@@ -637,24 +642,24 @@ function buildPreparedCursorRunRequest(
637
642
  readPaths: [],
638
643
  }),
639
644
  action,
640
- modelDetails: create(ModelDetailsSchema, {
641
- modelId: request.modelId,
642
- displayModelId: request.modelId,
643
- displayName: request.modelId,
644
- displayNameShort: request.modelId,
645
- aliases: [],
646
- }),
647
- // requested_model is currently a Cursor Router-only surface. External model clients still
648
- // send model_details alone; sending both makes external workers reach stepCompleted and then
649
- // reject the turn with invalid_argument.
650
- ...(request.routingLevel ? {
645
+ // Explicit model-picker parameters follow current Cursor clients and use requested_model alone.
646
+ // Keep legacy model_details for flat model ids and the already-live Router path; sending both for
647
+ // a parameterized external model can resolve conflicting selections and end in invalid_argument.
648
+ ...(!hasExplicitModelParameters ? {
649
+ modelDetails: create(ModelDetailsSchema, {
650
+ modelId: request.modelId,
651
+ displayModelId: request.modelId,
652
+ displayName: request.modelId,
653
+ displayNameShort: request.modelId,
654
+ aliases: [],
655
+ }),
656
+ } : {}),
657
+ ...(requestedModelParameters.length > 0 ? {
651
658
  requestedModel: create(RequestedModelSchema, {
652
659
  modelId: request.modelId,
653
660
  maxMode: false,
654
- parameters: [create(RequestedModel_ModelParameterbytesSchema, {
655
- id: CURSOR_ROUTING_LEVEL_PARAMETER_ID,
656
- value: request.routingLevel,
657
- })],
661
+ parameters: requestedModelParameters.map(parameter =>
662
+ create(RequestedModel_ModelParameterbytesSchema, parameter)),
658
663
  }),
659
664
  } : {}),
660
665
  // Mirror the client (Responses) tool definitions into the top-level AgentRunRequest.mcp_tools
@@ -8,7 +8,7 @@ import type {
8
8
  OcxToolResultMessage,
9
9
  } from "../../types";
10
10
  import { isAllowedToolChoice, namespacedToolName, toolChoiceAliases, type OcxTool, type OcxToolChoice } from "../../types";
11
- import type { CursorRequestMessage, CursorRunRequest } from "./types";
11
+ import type { CursorRequestMessage, CursorRequestedModelParameter, CursorRunRequest } from "./types";
12
12
  import { cursorWireModelSelection, type CursorRoutingLevel } from "./discovery";
13
13
  import { cursorEffortSuffix, cursorWireModelIdWithEffort } from "./effort-map";
14
14
  import {
@@ -117,16 +117,29 @@ function catalogLimitNote(kept: readonly OcxTool[], omitted: readonly OcxTool[])
117
117
  }
118
118
 
119
119
  /**
120
- * Resolve a `cursor/<model>` selection + Codex reasoning effort to the actual Cursor model id. Cursor
121
- * encodes the effort as a per-model suffix (`claude-4.6-opus-high`); `cursorEffortSuffix` picks the
122
- * right tier for that specific model (literal pass-through, with rank clamp fallback) or
123
- * `undefined` for non-reasoning models like `composer-2.5`. A fully-qualified id (one that isn't a
124
- * known effort base) passes through unchanged.
120
+ * Resolve a `cursor/<model>` selection + Codex reasoning effort to Cursor's requested model shape.
121
+ * Most models encode effort in a flat id (`claude-4.6-opus-high`). Grok 4.5 Fast is parameterized
122
+ * instead: current Cursor clients send the `grok-4.5` base id plus `effort` and `fast` parameters.
123
+ * A fully-qualified id (one that is not a known effort base) passes through unchanged.
125
124
  */
126
- function normalizeCursorModelId(modelId: string, reasoning?: string): { modelId: string; routingLevel?: CursorRoutingLevel } {
125
+ function normalizeCursorModelId(modelId: string, reasoning?: string): {
126
+ modelId: string;
127
+ requestedModelParameters?: readonly CursorRequestedModelParameter[];
128
+ routingLevel?: CursorRoutingLevel;
129
+ } {
127
130
  const selection = cursorWireModelSelection(modelId);
128
131
  const id = selection.modelId;
129
132
  const suffix = cursorEffortSuffix(id, reasoning);
133
+ if (id === "grok-4.5-fast" && suffix) {
134
+ return {
135
+ ...selection,
136
+ modelId: "grok-4.5",
137
+ requestedModelParameters: [
138
+ { id: "effort", value: suffix },
139
+ { id: "fast", value: "true" },
140
+ ],
141
+ };
142
+ }
130
143
  return { ...selection, modelId: suffix ? cursorWireModelIdWithEffort(id, suffix) : id };
131
144
  }
132
145
 
@@ -241,6 +254,7 @@ export function createCursorRequest(
241
254
  const model = normalizeCursorModelId(parsed.modelId, parsed.options.reasoning);
242
255
  return {
243
256
  modelId: model.modelId,
257
+ ...(model.requestedModelParameters ? { requestedModelParameters: model.requestedModelParameters } : {}),
244
258
  ...(model.routingLevel ? { routingLevel: model.routingLevel } : {}),
245
259
  conversationId: resolveCursorConversationId(parsed, model.modelId, options),
246
260
  system: [...(parsed.context.systemPrompt ?? []), ...(limitNote ? [limitNote] : [])],
@@ -2,8 +2,15 @@ import type { OcxUsage } from "../../types";
2
2
  import type { OcxMessage, OcxRequestOptions, OcxTool } from "../../types";
3
3
  import type { CursorRoutingLevel } from "./discovery";
4
4
 
5
+ export interface CursorRequestedModelParameter {
6
+ id: string;
7
+ value: string;
8
+ }
9
+
5
10
  export interface CursorRunRequest {
6
11
  modelId: string;
12
+ /** Cursor model-picker parameters encoded through AgentRunRequest.requested_model. */
13
+ requestedModelParameters?: readonly CursorRequestedModelParameter[];
7
14
  /** Cursor Router optimization parameter; valid only while modelId is the `default` wire model. */
8
15
  routingLevel?: CursorRoutingLevel;
9
16
  conversationId: string;