@bitkyc08/opencodex 2.25.0 → 2.26.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 (99) hide show
  1. package/bin/ocx.mjs +59 -7
  2. package/gui/dist/assets/index-RL6b1bTV.js +102 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/base.ts +18 -0
  6. package/src/adapters/client-fingerprint.ts +0 -2
  7. package/src/adapters/cursor/http1-bidi.ts +361 -0
  8. package/src/adapters/cursor/live-models.ts +117 -30
  9. package/src/adapters/cursor/live-transport.ts +137 -56
  10. package/src/adapters/cursor/native-exec-fs.ts +1 -1
  11. package/src/adapters/cursor/native-exec-network.ts +1 -1
  12. package/src/adapters/cursor/native-exec-shell.ts +0 -1
  13. package/src/adapters/cursor/protobuf-request.ts +89 -21
  14. package/src/adapters/cursor/tool-definitions.ts +18 -7
  15. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  16. package/src/adapters/cursor/transport.ts +7 -0
  17. package/src/adapters/cursor.ts +2 -0
  18. package/src/adapters/google-http.ts +38 -10
  19. package/src/adapters/google.ts +23 -3
  20. package/src/adapters/openai-chat.ts +38 -13
  21. package/src/adapters/openai-responses.ts +129 -9
  22. package/src/adapters/registry.ts +30 -1
  23. package/src/bridge.ts +50 -12
  24. package/src/chat/inbound.ts +1 -0
  25. package/src/claude/agents-inject.ts +3 -2
  26. package/src/cli/dispatch.ts +19 -6
  27. package/src/cli/help.ts +2 -1
  28. package/src/cli/integrations.ts +35 -0
  29. package/src/cli/minimax.ts +8 -2
  30. package/src/cli/registry.ts +13 -2
  31. package/src/clients/config-export.ts +120 -1
  32. package/src/codex/account-store.ts +17 -1
  33. package/src/codex/auth-api.ts +51 -17
  34. package/src/codex/catalog/effort.ts +8 -5
  35. package/src/codex/catalog/provider-fetch.ts +39 -27
  36. package/src/codex/catalog/sync.ts +30 -8
  37. package/src/codex/inject.ts +39 -13
  38. package/src/codex/main-account.ts +29 -1
  39. package/src/codex/plan-from-token.ts +140 -0
  40. package/src/codex/plan.ts +25 -0
  41. package/src/codex/prompt-journal.ts +6 -2
  42. package/src/codex/quota-rejection.ts +21 -7
  43. package/src/codex/refresh.ts +4 -2
  44. package/src/codex/sync.ts +106 -3
  45. package/src/codex/warmup.ts +187 -81
  46. package/src/config.ts +85 -46
  47. package/src/generated/compatibility-version.json +127 -83
  48. package/src/grok/inject.ts +1 -1
  49. package/src/images/loop.ts +1 -0
  50. package/src/integrations/registry.ts +7 -0
  51. package/src/lab/automation/config-persistence.ts +2 -2
  52. package/src/lab/automation/persistence.ts +2 -2
  53. package/src/lab/ledger/purge.ts +2 -2
  54. package/src/lab/subject/behavior-fingerprint.ts +2 -2
  55. package/src/lib/config-ownership.ts +5 -2
  56. package/src/lib/destination-policy.ts +18 -1
  57. package/src/lib/provider-outbound.ts +7 -0
  58. package/src/lib/redact.ts +11 -0
  59. package/src/lib/tool-argument-integers.ts +50 -6
  60. package/src/lib/upstream-http-version.ts +57 -0
  61. package/src/lib/upstream-retry.ts +2 -1
  62. package/src/lib/windows-atomic-replace.ts +155 -0
  63. package/src/lib/windows-service-wrappers.ts +72 -0
  64. package/src/oauth/google-antigravity.ts +2 -2
  65. package/src/oauth/index.ts +60 -8
  66. package/src/providers/antigravity-models.ts +58 -4
  67. package/src/providers/command-code-efforts.ts +36 -9
  68. package/src/providers/context-cap.ts +9 -0
  69. package/src/providers/derive.ts +4 -0
  70. package/src/providers/fastwire.ts +453 -0
  71. package/src/providers/registry.ts +21 -1
  72. package/src/providers/service-tier.ts +173 -89
  73. package/src/responses/parser.ts +25 -16
  74. package/src/responses/reasoning-replay-cache.ts +30 -0
  75. package/src/responses/thought-signature-replay.ts +74 -4
  76. package/src/router.ts +6 -0
  77. package/src/routing/compatibility/behavior.ts +27 -16
  78. package/src/server/index.ts +10 -1
  79. package/src/server/management/oauth-account-routes.ts +10 -2
  80. package/src/server/management/shared.ts +1 -1
  81. package/src/server/management/system-routes.ts +15 -0
  82. package/src/server/request-log.ts +57 -3
  83. package/src/server/responses/agent-task-recovery.ts +6 -1
  84. package/src/server/responses/core.ts +209 -40
  85. package/src/server/responses/fetch-helpers.ts +15 -36
  86. package/src/server/responses/responses-field-backfill.ts +173 -0
  87. package/src/server/responses-reasoning-summary-rewrite.ts +171 -0
  88. package/src/service.ts +38 -36
  89. package/src/storage/cleanup.ts +2 -2
  90. package/src/tray/windows.ts +3 -2
  91. package/src/types.ts +101 -14
  92. package/src/update/job.ts +9 -18
  93. package/src/update/transactional-install.d.mts +22 -0
  94. package/src/update/transactional-install.mjs +259 -0
  95. package/src/usage/cost.ts +34 -5
  96. package/src/usage/log.ts +74 -4
  97. package/src/vision/anthropic-describe.ts +10 -6
  98. package/src/web-search/anthropic-executor.ts +8 -6
  99. package/gui/dist/assets/index-DxJ7kXj9.js +0 -102
@@ -81,6 +81,8 @@ import {
81
81
  } from "./native-exec-shell";
82
82
  import type { CursorClientMessage, CursorRunRequest, CursorServerMessage } from "./types";
83
83
  import type { CursorTransport, CursorTransportFactoryInput } from "./transport";
84
+ import { CursorHttp1BidiConnection } from "./http1-bidi";
85
+ import { isPinnedHttp1 } from "../../lib/upstream-http-version";
84
86
 
85
87
  const CURSOR_RUN_PATH = "/agent.v1.AgentService/Run";
86
88
  const CURSOR_CLIENT_VERSION = "cli-2026.07.08-0c04a8a";
@@ -406,6 +408,7 @@ export function clientToolFinalizeGraceMsForRequest(request: CursorRunRequest, b
406
408
  class LiveCursorTransport implements CursorTransport {
407
409
  private session?: http2.ClientHttp2Session;
408
410
  private stream?: http2.ClientHttp2Stream;
411
+ private http1Connection?: CursorHttp1BidiConnection;
409
412
  private heartbeat?: ReturnType<typeof setInterval>;
410
413
  private firstFrameTimer?: ReturnType<typeof setTimeout>;
411
414
  private committed = false;
@@ -438,9 +441,12 @@ class LiveCursorTransport implements CursorTransport {
438
441
  private firstFrameAt?: number;
439
442
  private firstFrameLogged = false;
440
443
  /** Stable session identifier sent as x-session-id; mirrors IDE session semantics. */
441
- private readonly sessionId = crypto.randomUUID();
444
+ private readonly sessionId: string;
445
+ /** Per-transport owner for native-exec / background shells. Must not share conversationId. */
446
+ private readonly shellOwnerId = crypto.randomUUID();
442
447
 
443
448
  constructor(private readonly input: CursorTransportFactoryInput) {
449
+ this.sessionId = input.sessionId?.trim() || crypto.randomUUID();
444
450
  this.translatorBudget = input.translatorBudget;
445
451
  this.token = resolveCursorToken(input.provider, input.headers);
446
452
  // Grace window before a drained client-tool turn is finalized. Small enough not to look like a
@@ -452,7 +458,7 @@ class LiveCursorTransport implements CursorTransport {
452
458
  this.desktopDeps = desktopDepsFromConfig(input.provider.desktopExecutor);
453
459
  this.execContext = {
454
460
  ...this.desktopDeps,
455
- sessionId: this.sessionId,
461
+ sessionId: this.shellOwnerId,
456
462
  unsafeAllowNativeLocalExec: effectiveCursorNativeExecAllow(input.provider, input.requestDeclaresFullAccess === true),
457
463
  };
458
464
  const servers = resolveMcpServers(input.provider);
@@ -487,7 +493,7 @@ class LiveCursorTransport implements CursorTransport {
487
493
  ...this.desktopDeps,
488
494
  ...mcpDepsFromManager(this.mcpManager!),
489
495
  mcpToolDefs,
490
- sessionId: this.sessionId,
496
+ sessionId: this.shellOwnerId,
491
497
  unsafeAllowNativeLocalExec: effectiveCursorNativeExecAllow(this.input.provider, this.input.requestDeclaresFullAccess === true),
492
498
  };
493
499
  } catch (err) {
@@ -680,12 +686,24 @@ class LiveCursorTransport implements CursorTransport {
680
686
  || this.pendingTransportFrames >= CURSOR_MAX_PENDING_FRAMES
681
687
  ) {
682
688
  this.stream?.pause();
689
+ this.http1Connection?.pause();
683
690
  return;
684
691
  }
685
692
  if (
686
693
  this.transportBufferedBytes <= CURSOR_TRANSPORT_RESUME_BYTES
687
694
  && this.pendingTransportFrames <= CURSOR_PENDING_FRAMES_RESUME
688
- ) this.stream?.resume();
695
+ ) {
696
+ this.stream?.resume();
697
+ this.http1Connection?.resume();
698
+ }
699
+ }
700
+
701
+ private writeConnectFrame(frame: Uint8Array): void {
702
+ if (this.http1Connection) {
703
+ this.http1Connection.write(frame);
704
+ return;
705
+ }
706
+ this.stream?.write(frame);
689
707
  }
690
708
 
691
709
  requestCommitted(): boolean {
@@ -700,7 +718,7 @@ class LiveCursorTransport implements CursorTransport {
700
718
  }
701
719
 
702
720
  private startShellCleanup(): Promise<BackgroundShellTerminationReport> {
703
- return this.shellCleanup ??= terminateBackgroundShellsForSession(this.sessionId);
721
+ return this.shellCleanup ??= terminateBackgroundShellsForSession(this.shellOwnerId);
704
722
  }
705
723
 
706
724
  async close(): Promise<void> {
@@ -709,6 +727,7 @@ class LiveCursorTransport implements CursorTransport {
709
727
  this.clearFirstFrameTimer();
710
728
  this.stream?.close();
711
729
  this.session?.close();
730
+ this.http1Connection?.close();
712
731
  this.releaseBlobRequestScope();
713
732
  this.releaseMcpObservation?.();
714
733
  this.releaseMcpObservation = undefined;
@@ -721,12 +740,16 @@ class LiveCursorTransport implements CursorTransport {
721
740
  this.clearPendingFinalize();
722
741
  if (this.heartbeat) clearInterval(this.heartbeat);
723
742
  this.clearFirstFrameTimer();
724
- try {
725
- this.stream?.close(http2.constants.NGHTTP2_CANCEL);
726
- } catch {
727
- this.stream?.destroy();
743
+ if (this.http1Connection) {
744
+ this.http1Connection.close();
745
+ } else {
746
+ try {
747
+ this.stream?.close(http2.constants.NGHTTP2_CANCEL);
748
+ } catch {
749
+ this.stream?.destroy();
750
+ }
751
+ this.session?.close();
728
752
  }
729
- this.session?.close();
730
753
  this.releaseBlobRequestScope();
731
754
  this.releaseMcpObservation?.();
732
755
  this.releaseMcpObservation = undefined;
@@ -791,35 +814,52 @@ class LiveCursorTransport implements CursorTransport {
791
814
  fail: (error: Error) => void,
792
815
  finish: () => void,
793
816
  ): void {
817
+ if (signal?.aborted) {
818
+ fail(signal.reason instanceof Error ? signal.reason : new Error("Cursor request was aborted"));
819
+ return;
820
+ }
794
821
  this.turnStartedAt = Date.now();
795
822
  this.framesReceived = 0;
796
823
  this.sawAssistantText = false;
797
824
  this.emittedTerminal = false;
798
825
  this.firstFrameAt = undefined;
799
826
  this.firstFrameLogged = false;
800
- const dialHost = cursorHostLabel(this.input.provider.baseUrl || "https://api2.cursor.sh");
801
- debugProviderDiagnostic("cursor", "dial", { host: dialHost });
802
- this.session = http2.connect(this.input.provider.baseUrl || "https://api2.cursor.sh");
803
- // The run request is buffered until the HTTP/2 session connects. Failures before `connect`
804
- // (DNS, ECONNREFUSED, TLS, connect timeout) mean the server never received the request, so they
805
- // are safe to retry. Once connected, bytes flush to the server and the turn must not be replayed.
806
- this.session.on("connect", () => {
807
- this.committed = true;
808
- debugProviderDiagnostic("cursor", "connected", { connectMs: Date.now() - this.turnStartedAt });
809
- });
810
- this.stream = this.session.request({
811
- ":method": "POST",
812
- ":path": CURSOR_RUN_PATH,
813
- "content-type": "application/connect+proto",
814
- "connect-protocol-version": "1",
815
- te: "trailers",
816
- authorization: `Bearer ${this.token}`,
817
- "x-ghost-mode": "true",
818
- "x-cursor-client-version": CURSOR_CLIENT_VERSION,
819
- "x-cursor-client-type": "cli",
820
- "x-request-id": crypto.randomUUID(),
821
- "x-session-id": this.sessionId,
822
- });
827
+ const baseUrl = this.input.provider.baseUrl || "https://api2.cursor.sh";
828
+ const useHttp1 = isPinnedHttp1(this.input.provider.upstreamHttpVersion);
829
+ const requestId = crypto.randomUUID();
830
+ const dialHost = cursorHostLabel(baseUrl);
831
+ debugProviderDiagnostic("cursor", "dial", { host: dialHost, transport: useHttp1 ? "http1.1" : "http2" });
832
+
833
+ let session: http2.ClientHttp2Session | undefined;
834
+ let stream: http2.ClientHttp2Stream | undefined;
835
+ if (!useHttp1) {
836
+ session = http2.connect(baseUrl);
837
+ this.session = session;
838
+ // The run request is buffered until the HTTP/2 session connects. Failures before `connect`
839
+ // (DNS, ECONNREFUSED, TLS, connect timeout) mean the server never received the request, so they
840
+ // are safe to retry. Once connected, bytes flush to the server and the turn must not be replayed.
841
+ session.on("connect", () => {
842
+ this.committed = true;
843
+ debugProviderDiagnostic("cursor", "connected", {
844
+ transport: "http2",
845
+ connectMs: Date.now() - this.turnStartedAt,
846
+ });
847
+ });
848
+ stream = session.request({
849
+ ":method": "POST",
850
+ ":path": CURSOR_RUN_PATH,
851
+ "content-type": "application/connect+proto",
852
+ "connect-protocol-version": "1",
853
+ te: "trailers",
854
+ authorization: `Bearer ${this.token}`,
855
+ "x-ghost-mode": "true",
856
+ "x-cursor-client-version": CURSOR_CLIENT_VERSION,
857
+ "x-cursor-client-type": "cli",
858
+ "x-request-id": requestId,
859
+ "x-session-id": this.sessionId,
860
+ });
861
+ this.stream = stream;
862
+ }
823
863
 
824
864
  // Single-shot terminal owner for this turn (createTerminalSettler): stream error, session
825
865
  // error, trailers, end, abort, and the first-frame timeout all race into it, and only the
@@ -845,11 +885,9 @@ class LiveCursorTransport implements CursorTransport {
845
885
  }
846
886
  settler.settleFail(error);
847
887
  };
848
- const session = this.session;
849
- const stream = this.stream;
850
888
  // Session-level errors (TLS/socket/GOAWAY) do not always propagate to the stream listener;
851
889
  // without this handler they could bypass orderly failure reporting entirely.
852
- session.on("error", err => {
890
+ const onSessionError = (err: unknown) => {
853
891
  const realErr = err instanceof Error ? err : new Error(String(err));
854
892
  debugProviderDiagnostic("cursor", "session-error", {
855
893
  code: String((realErr as { code?: unknown }).code ?? ""),
@@ -857,15 +895,18 @@ class LiveCursorTransport implements CursorTransport {
857
895
  elapsedMs: Date.now() - this.turnStartedAt,
858
896
  });
859
897
  failAndClear(realErr);
860
- });
898
+ };
861
899
  this.firstFrameTimer = setTimeout(() => {
862
900
  this.firstFrameTimer = undefined;
863
901
  debugProviderDiagnostic("cursor", "first-frame-timeout", { timeoutMs: this.input.firstFrameTimeoutMs ?? CURSOR_FIRST_FRAME_TIMEOUT_MS });
864
- try { stream.close(); } catch { /* already closing */ }
865
- try { session.close(); } catch { /* already closing */ }
866
- // close() waits for in-flight frames; a dead socket can ignore it — force-destroy shortly
867
- // after so a stalled TLS session cannot linger past the timeout.
868
- armTimeoutDestroyFallback(stream, session, this.input.timeoutDestroyGraceMs ?? CURSOR_TIMEOUT_DESTROY_GRACE_MS);
902
+ try { stream?.close(); } catch { /* already closing */ }
903
+ try { session?.close(); } catch { /* already closing */ }
904
+ this.http1Connection?.close();
905
+ if (stream && session) {
906
+ // close() waits for in-flight frames; a dead socket can ignore it — force-destroy shortly
907
+ // after so a stalled TLS session cannot linger past the timeout.
908
+ armTimeoutDestroyFallback(stream, session, this.input.timeoutDestroyGraceMs ?? CURSOR_TIMEOUT_DESTROY_GRACE_MS);
909
+ }
869
910
  releaseBacklogLease();
870
911
  settler.settleFail(new Error("Cursor transport timed out before first response"));
871
912
  }, this.input.firstFrameTimeoutMs ?? CURSOR_FIRST_FRAME_TIMEOUT_MS);
@@ -963,7 +1004,7 @@ class LiveCursorTransport implements CursorTransport {
963
1004
  });
964
1005
  }
965
1006
  };
966
- this.stream.on("data", chunk => {
1007
+ const onData = (chunk: string | Uint8Array) => {
967
1008
  this.clearFirstFrameTimer();
968
1009
  // Once the turn has settled, late network bytes must never be charged —
969
1010
  // the backlog lease is already released and nobody would own these.
@@ -992,13 +1033,13 @@ class LiveCursorTransport implements CursorTransport {
992
1033
  if (charged && !appended) this.releaseTransportBytes(bytes.byteLength);
993
1034
  failAndClear(err instanceof Error ? err : new Error(String(err)));
994
1035
  }
995
- });
996
- this.stream.on("trailers", trailers => {
1036
+ };
1037
+ const onTrailers = (trailers: http2.IncomingHttpHeaders) => {
997
1038
  const status = trailers["grpc-status"];
998
1039
  if (status !== undefined) debugProviderDiagnostic("cursor", "trailers", { grpcStatus: String(status) });
999
1040
  if (status && status !== "0") failAndClear(new Error(`Cursor gRPC error ${status}`));
1000
- });
1001
- this.stream.on("error", err => {
1041
+ };
1042
+ const onStreamError = (err: unknown) => {
1002
1043
  const realErr = err instanceof Error ? err : new Error(String(err));
1003
1044
  if (this.expectedClose) {
1004
1045
  failAndClear(realErr);
@@ -1016,8 +1057,8 @@ class LiveCursorTransport implements CursorTransport {
1016
1057
  elapsedMs: Date.now() - this.turnStartedAt,
1017
1058
  });
1018
1059
  failAndClear(realErr);
1019
- });
1020
- this.stream.on("end", () => {
1060
+ };
1061
+ const onStreamEnd = () => {
1021
1062
  this.clearFirstFrameTimer();
1022
1063
  debugProviderDiagnostic("cursor", "stream-end", {
1023
1064
  committed: this.committed,
@@ -1078,16 +1119,56 @@ class LiveCursorTransport implements CursorTransport {
1078
1119
  }, (err) => {
1079
1120
  failAndClear(err instanceof Error ? err : new Error(String(err)));
1080
1121
  });
1081
- });
1122
+ };
1123
+
1124
+ if (useHttp1) {
1125
+ const providerFetch = this.input.fetch
1126
+ ?? (this.input.provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch;
1127
+ this.http1Connection = new CursorHttp1BidiConnection({
1128
+ baseUrl,
1129
+ token: this.token,
1130
+ clientVersion: CURSOR_CLIENT_VERSION,
1131
+ sessionId: this.sessionId,
1132
+ requestId,
1133
+ translatorBudget: this.translatorBudget,
1134
+ callbacks: {
1135
+ onCommitted: () => {
1136
+ this.committed = true;
1137
+ debugProviderDiagnostic("cursor", "connected", {
1138
+ transport: "http1.1",
1139
+ connectMs: Date.now() - this.turnStartedAt,
1140
+ });
1141
+ },
1142
+ onData,
1143
+ onEnd: onStreamEnd,
1144
+ onError: onStreamError,
1145
+ },
1146
+ ...(providerFetch ? { fetch: providerFetch } : {}),
1147
+ });
1148
+ this.http1Connection.start();
1149
+ } else {
1150
+ session!.on("error", onSessionError);
1151
+ stream!.on("data", onData);
1152
+ stream!.on("trailers", onTrailers);
1153
+ stream!.on("error", onStreamError);
1154
+ stream!.on("end", onStreamEnd);
1155
+ }
1082
1156
 
1083
1157
  signal?.addEventListener("abort", () => {
1084
1158
  this.close();
1085
1159
  failAndClear(new Error("Cursor request was aborted"));
1086
1160
  }, { once: true });
1161
+ // Close the race between the preflight above and listener installation. No request payload is
1162
+ // written until after this check.
1163
+ if (signal?.aborted) {
1164
+ this.close();
1165
+ failAndClear(signal.reason instanceof Error ? signal.reason : new Error("Cursor request was aborted"));
1166
+ return;
1167
+ }
1087
1168
 
1088
- this.stream.write(encodeConnectFrame(encodedRequest));
1169
+ this.writeConnectFrame(encodeConnectFrame(encodedRequest));
1089
1170
  this.heartbeat = setInterval(() => {
1090
- this.stream?.write(encodeClientMessage({
1171
+ this.writeConnectFrame(encodeClientMessage({
1091
1172
  message: { case: "clientHeartbeat", value: create(ClientHeartbeatSchema, {}) },
1092
1173
  }));
1093
1174
  }, HEARTBEAT_MS);
@@ -1098,10 +1179,10 @@ class LiveCursorTransport implements CursorTransport {
1098
1179
  state: ReturnType<typeof createCursorProtobufEventState>,
1099
1180
  push: (message: CursorServerMessage) => void,
1100
1181
  ): Promise<void> {
1101
- if (!this.stream) return;
1182
+ if (!this.stream && !this.http1Connection) return;
1102
1183
  debugProviderDiagnostic("cursor", "frame", describeCursorServerFrame(message));
1103
1184
  if (message.message.case === "kvServerMessage") {
1104
- this.stream.write(encodeConnectFrame(handleCursorNativeKv(message.message.value, this.blobRequestScope)));
1185
+ this.writeConnectFrame(encodeConnectFrame(handleCursorNativeKv(message.message.value, this.blobRequestScope)));
1105
1186
  return;
1106
1187
  }
1107
1188
  if (message.message.case === "execServerMessage") {
@@ -1122,7 +1203,7 @@ class LiveCursorTransport implements CursorTransport {
1122
1203
  // run the same local action twice.
1123
1204
  push({ type: "local_side_effect" });
1124
1205
  const replies = await handleCursorNativeExec(message.message.value, this.execContext);
1125
- for (const reply of replies) this.stream.write(encodeConnectFrame(reply));
1206
+ for (const reply of replies) this.writeConnectFrame(encodeConnectFrame(reply));
1126
1207
  return;
1127
1208
  }
1128
1209
  if (message.message.case === "interactionQuery") {
@@ -1132,7 +1213,7 @@ class LiveCursorTransport implements CursorTransport {
1132
1213
  const query = message.message.value;
1133
1214
  const plan = planInteractionQueryReply(query);
1134
1215
  debugProviderDiagnostic("cursor", "interaction-query", { id: query.id, queryCase: query.query.case ?? "unknown", reply: plan.replyCase });
1135
- this.stream.write(encodeClientMessage({ message: { case: "interactionResponse", value: plan.response } }));
1216
+ this.writeConnectFrame(encodeClientMessage({ message: { case: "interactionResponse", value: plan.response } }));
1136
1217
  if (!state.terminated) {
1137
1218
  if (plan.planText) {
1138
1219
  this.sawAssistantText = true;
@@ -47,7 +47,7 @@ function codexNativeMutationRefusal(operation: "write" | "delete", structuredEdi
47
47
  }
48
48
 
49
49
  const NATIVE_LOCAL_EXEC_DISABLED =
50
- "Route filesystem work through the Codex Responses bridge shell tool (`shell_command` / `exec_command`, or the listed `mcp_opencodex-responses_*` display alias) with equivalent shell commands (cat, head, ls, rg, grep), or `apply_patch` for file edits. Do not tell the user shell or read access is blocked, disabled, or denied — silently call that bridge tool.";
50
+ "Cursor-native filesystem tools are not executed locally. Use a catalog tool for this work: `shell_command` / `exec_command` (or the listed `mcp_opencodex-responses_*` display alias) with host-shell-safe equivalents: POSIX (`cat`, `head`, `ls`, `rg`, `grep`) or Windows PowerShell (`Get-Content`, `Get-ChildItem`, `Select-String`); use `apply_patch` for file edits.";
51
51
 
52
52
  export function rejectReadExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
53
53
  if (execMsg.message.case !== "readArgs") throw new Error("invalid read exec");
@@ -7,7 +7,7 @@ export interface CursorNativeNetworkDeps {
7
7
  }
8
8
 
9
9
  const NATIVE_FETCH_DISABLED =
10
- "Route this through the Codex shell bridge tool `shell_command` (aliases: `exec_command`, `mcp_opencodex-responses_shell_command`, `mcp_opencodex-responses_exec_command`) with curl or wget. Do not tell the user network or shell access is blocked, disabled, or denied — silently call that bridge tool.";
10
+ "Cursor-native fetch is not executed locally. Use the Codex shell bridge tool `shell_command` (aliases: `exec_command`, `mcp_opencodex-responses_shell_command`, `mcp_opencodex-responses_exec_command`) with curl or wget.";
11
11
 
12
12
  export function rejectFetchExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
13
13
  if (execMsg.message.case !== "fetchArgs") throw new Error("invalid fetch exec");
@@ -89,7 +89,6 @@ export function nativeShellDisabledMessage(): string {
89
89
  // client that executes the bridge (LAN/SSH remote-proxy).
90
90
  return (
91
91
  "Route this through the Codex bridge shell tool from the current catalog (`shell_command` or `exec_command`, including the long `mcp_opencodex-responses_*` display name if listed). "
92
- + "Do not tell the user shell access is blocked, disabled, or denied — silently call that bridge tool. "
93
92
  + "Adapt the command for the Codex client host shell before calling the bridge "
94
93
  + "(Windows PowerShell 5.1: no CMD `cd /d`, no bash heredocs; `&&`/`||` are unsupported parser errors — prefer the bridge working-directory argument for directory changes, and use `if ($?) { ... }` for success-gated follow-up steps; do not treat `;` as a substitute for `&&`). "
95
94
  + "Make at most one corrected bridge attempt after a failure, then report the error and stop — do not repeat equivalent failing commands."
@@ -5,6 +5,7 @@ import type { OcxAssistantContentPart, OcxMessage, OcxToolResultMessage } from "
5
5
  import { namespacedToolName } from "../../types";
6
6
  import type { CursorRunRequest } from "./types";
7
7
  import { isCursorExternalWireModel } from "./discovery";
8
+ import { normalizeCursorToolResultText } from "./tool-result-normalize";
8
9
  import { debugProviderDiagnostic } from "../../lib/debug";
9
10
  import {
10
11
  createCursorBlobRequestScope,
@@ -68,6 +69,14 @@ export const CURSOR_EXTERNAL_ROOT_BLOB_LIMIT = 192;
68
69
  /** Approximate prompt-size guard; tool schemas and protocol framing consume context separately. */
69
70
  export const CURSOR_EXTERNAL_ROOT_BYTE_LIMIT = 512 * 1024;
70
71
 
72
+ /**
73
+ * Action text for external-model tool-result continuations. Native models keep
74
+ * resumeAction; external wire models continue as userMessageAction so the
75
+ * results already stored in history blobs are visible without a ResumeAction.
76
+ */
77
+ export const CURSOR_EXTERNAL_TOOL_CONTINUATION_TEXT =
78
+ "Continue: the requested tool results are provided in the conversation history above.";
79
+
71
80
  /** Runtime timezone for protobuf RequestContextEnv (dynamic, never hardcoded). */
72
81
  function runtimeTimeZone(): string {
73
82
  try {
@@ -122,6 +131,10 @@ function rootBlobCandidate(
122
131
  };
123
132
  }
124
133
 
134
+ function toolResultRootPayload(text: string): { role: "assistant"; content: [{ type: "text"; text: string }] } {
135
+ return { role: "assistant", content: [{ type: "text", text }] };
136
+ }
137
+
125
138
  function truncateToolResultBlob(entry: RootBlobCandidate, maxBytes: number): RootBlobCandidate | null {
126
139
  if (entry.byteLength <= maxBytes) return entry;
127
140
  if (entry.role !== "toolResult" || entry.text === undefined) return null;
@@ -134,7 +147,7 @@ function truncateToolResultBlob(entry: RootBlobCandidate, maxBytes: number): Roo
134
147
  while (end > 0 && end < encoded.byteLength && (encoded[end]! & 0xc0) === 0x80) end -= 1;
135
148
  const truncated = `${decoder.decode(encoded.subarray(0, end))}${marker}`;
136
149
  const result = rootBlobCandidate(
137
- { role: "user", content: [{ type: "text", text: truncated }] },
150
+ toolResultRootPayload(truncated),
138
151
  "toolResult",
139
152
  { messageIndex: entry.messageIndex, text: truncated },
140
153
  );
@@ -143,7 +156,7 @@ function truncateToolResultBlob(entry: RootBlobCandidate, maxBytes: number): Roo
143
156
  keepBytes = Math.max(0, end - (result.byteLength - maxBytes) - 16);
144
157
  }
145
158
  const markerOnly = rootBlobCandidate(
146
- { role: "user", content: [{ type: "text", text: marker.trimStart() }] },
159
+ toolResultRootPayload(marker.trimStart()),
147
160
  "toolResult",
148
161
  { messageIndex: entry.messageIndex, text: marker.trimStart() },
149
162
  );
@@ -175,9 +188,8 @@ function assistantRootText(
175
188
  // Cursor builds the actual model prompt from rootPromptMessagesJson (turns[] is UI/display metadata),
176
189
  // so prior history — including assistant tool calls and tool results — must be replayed here or a
177
190
  // ResumeAction has nothing model-visible to continue from. The active user message is excluded
178
- // because it travels in the action. Tool results are rendered as user-role text with a marker, and
179
- // each entry is a SHA-256 blob ID (Cursor fetches the bytes back via getBlobArgs). Mirrors the
180
- // danger-pi reference buildRootPromptMessagesJson.
191
+ // because it travels in the action. Tool results are assistant-role text with a [Tool Result]
192
+ // or [Tool Error] marker so Cursor does not wrap them as `<user_query>` (#1992). Each entry is a SHA-256 blob ID.
181
193
  function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobRequestScopeToken): {
182
194
  ids: Uint8Array[];
183
195
  byteLength: number;
@@ -229,10 +241,12 @@ function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobR
229
241
  }
230
242
  // Assistant tool CALLS are intentionally NOT replayed as visible "[Tool Call]" text here.
231
243
  } else if (message.role === "toolResult") {
232
- const prefix = message.isError ? "[Tool Error]" : "[Tool Result]";
244
+ // #1920: the prefix must reflect the NORMALIZED error state (an empty
245
+ // node_repl result is an error even when the runtime said isError=false).
246
+ const prefix = normalizedToolResult(message, contentToText(message.content)).isError ? "[Tool Error]" : "[Tool Result]";
233
247
  const text = `${prefix}\n${toolResultToText(message)}`;
234
248
  entries.push(rootBlobCandidate(
235
- { role: "user", content: [{ type: "text", text }] },
249
+ toolResultRootPayload(text),
236
250
  "toolResult",
237
251
  { messageIndex: i, text },
238
252
  ));
@@ -382,6 +396,8 @@ type DecodedResultPart =
382
396
  | { kind: "image"; bytes: Uint8Array; mimeType: string }
383
397
  | { kind: "undecodable" };
384
398
 
399
+ type NormalizedToolResult = { text: string; isError: boolean };
400
+
385
401
  /**
386
402
  * Decode a tool result's parts ONCE. `toolCallStep` may re-serialize a step several times while
387
403
  * shrinking it to fit blob admission, and decoding base64 on every attempt made that loop
@@ -413,13 +429,24 @@ function toolResultContentItems(
413
429
  message: OcxToolResultMessage,
414
430
  decoded?: DecodedResultPart[],
415
431
  maxImages = Number.POSITIVE_INFINITY,
432
+ normalizedText?: NormalizedToolResult,
416
433
  ) {
417
434
  const parts = decoded ?? decodeResultParts(message);
435
+ const textItem = (text: string) => [create(McpToolResultContentItemSchema, {
436
+ content: { case: "text" as const, value: create(McpTextContentSchema, { text }) },
437
+ })];
418
438
  if (!parts) {
419
- const text = typeof message.content === "string" ? message.content : "";
420
- return [create(McpToolResultContentItemSchema, {
421
- content: { case: "text" as const, value: create(McpTextContentSchema, { text }) },
422
- })];
439
+ const normalized = normalizedText
440
+ ?? normalizedToolResult(message, typeof message.content === "string" ? message.content : "");
441
+ return textItem(normalized.text);
442
+ }
443
+ const normalized = normalizedText ?? normalizedDecodedTextResult(message, parts);
444
+ if (normalized) {
445
+ // #1920/#1866: empty or failure-state Computer Use / node_repl results are
446
+ // normalized before they reach the native wire. Pure-text part arrays use
447
+ // the same newline-joined representation this serializer already emitted;
448
+ // image-bearing and undecodable results stay on the lossless part path.
449
+ return textItem(normalized.text);
423
450
  }
424
451
  // Images are dropped OLDEST first when the step must shrink: the most recent screenshot is the
425
452
  // one the model is reasoning about, so it is the last to go.
@@ -472,16 +499,43 @@ function toolResultContentItems(
472
499
  }
473
500
 
474
501
  function toolResultToText(message: OcxToolResultMessage): string {
502
+ const normalized = normalizedToolResult(message, contentToText(message.content));
475
503
  return [
476
504
  "[tool_result]",
477
505
  `call_id: ${message.toolCallId}`,
478
506
  `name: ${namespacedToolName(message.toolNamespace, message.toolName)}`,
479
- `is_error: ${message.isError}`,
507
+ `is_error: ${normalized.isError}`,
480
508
  "output:",
481
- contentToText(message.content),
509
+ normalized.text,
482
510
  ].join("\n");
483
511
  }
484
512
 
513
+ /**
514
+ * Shared #1920 normalization entry: pure-text results only. Image-bearing or
515
+ * encrypted results pass through untouched (their content is not plain text).
516
+ */
517
+ function normalizedToolResult(message: OcxToolResultMessage, text: string): NormalizedToolResult {
518
+ if (message.containsEncryptedContent) return { text, isError: message.isError };
519
+ return normalizeCursorToolResultText(text, {
520
+ toolName: message.toolName,
521
+ toolNamespace: message.toolNamespace,
522
+ isError: message.isError,
523
+ });
524
+ }
525
+
526
+ /**
527
+ * A content-part result is plain text only when every decoded part is text (the empty array is the
528
+ * empty text result). Join it exactly as toolResultContentItems already did, then share the string
529
+ * normalization contract. Any image or undecodable part keeps the existing part-preserving path.
530
+ */
531
+ function normalizedDecodedTextResult(
532
+ message: OcxToolResultMessage,
533
+ parts: DecodedResultPart[],
534
+ ): NormalizedToolResult | undefined {
535
+ if (parts.some(part => part.kind !== "text")) return undefined;
536
+ return normalizedToolResult(message, parts.map(part => part.kind === "text" ? part.text : "").join("\n"));
537
+ }
538
+
485
539
  function argBytes(value: unknown): Uint8Array {
486
540
  try {
487
541
  return toBinary(ValueSchema, fromJson(ValueSchema, value as JsonValue));
@@ -535,12 +589,16 @@ function toolCallStep(
535
589
  }
536
590
 
537
591
  function toolResultPart(message: OcxToolResultMessage, decoded?: DecodedResultPart[], maxImages?: number) {
592
+ const parts = decoded ?? decodeResultParts(message);
593
+ const normalized = parts
594
+ ? normalizedDecodedTextResult(message, parts)
595
+ : normalizedToolResult(message, typeof message.content === "string" ? message.content : "");
538
596
  return create(McpToolResultSchema, {
539
597
  result: {
540
598
  case: "success",
541
599
  value: create(McpSuccessSchema, {
542
- isError: message.isError,
543
- content: toolResultContentItems(message, decoded, maxImages),
600
+ isError: normalized?.isError ?? message.isError,
601
+ content: toolResultContentItems(message, parts, maxImages, normalized),
544
602
  }),
545
603
  },
546
604
  });
@@ -632,11 +690,15 @@ function conversationTurns(
632
690
  if (message.role === "toolResult") {
633
691
  if (!current) continue;
634
692
  if (externalModel) {
635
- const prefix = message.isError ? "[Tool Error]" : "[Tool Result]";
693
+ // #1920/#1866: this external-replay site bypasses toolResultToText, so it
694
+ // must consume the normalizer directly — cursor/grok-4.6 is the exact
695
+ // reported repro path for empty Computer Use results.
696
+ const normalized = normalizedToolResult(message, contentToText(message.content));
697
+ const prefix = normalized.isError ? "[Tool Error]" : "[Tool Result]";
636
698
  current.steps.push(storeCursorBlob(toBinary(ConversationStepSchema, create(ConversationStepSchema, {
637
699
  message: {
638
700
  case: "assistantMessage",
639
- value: create(AssistantMessageSchema, { text: `${prefix}\n${contentToText(message.content)}` }),
701
+ value: create(AssistantMessageSchema, { text: `${prefix}\n${normalized.text}` }),
640
702
  },
641
703
  })), requestScope));
642
704
  continue;
@@ -728,18 +790,24 @@ function buildPreparedCursorRunRequest(
728
790
  const text = lastRole === "user" || lastRole === "developer"
729
791
  ? appendCursorGenericToolUseHint(request.tools, rawText)
730
792
  : rawText;
731
- // Tool-result-only turns resume the remembered Cursor conversation with results in history.
732
793
  const lastRawIsToolResult = request.rawMessages?.at(-1)?.role === "toolResult";
733
- const actionCase = !lastRawIsToolResult && text.trim().length > 0
794
+ // Native models resume the remembered Cursor conversation. External wire
795
+ // models continue as userMessageAction so history-blob tool results stay
796
+ // visible without a ResumeAction.
797
+ const externalToolContinuation = lastRawIsToolResult && isCursorExternalWireModel(request.modelId);
798
+ const actionCase = (externalToolContinuation || (!lastRawIsToolResult && text.trim().length > 0))
734
799
  ? "userMessageAction"
735
800
  : "resumeAction";
801
+ const actionText = externalToolContinuation
802
+ ? CURSOR_EXTERNAL_TOOL_CONTINUATION_TEXT
803
+ : text;
736
804
  const action = create(ConversationActionSchema, {
737
805
  action: actionCase === "userMessageAction"
738
806
  ? {
739
807
  case: "userMessageAction",
740
808
  value: create(UserMessageActionSchema, {
741
809
  userMessage: create(UserMessageSchema, {
742
- text,
810
+ text: actionText,
743
811
  messageId: crypto.randomUUID(),
744
812
  }),
745
813
  requestContext: buildRequestContext(),
@@ -839,7 +907,7 @@ function buildPreparedCursorRunRequest(
839
907
  // tools the payload dropped — the defect that blocked PR #376.
840
908
  const modelVisibleParts = [
841
909
  ...rootPromptMessagesState.serialized,
842
- ...(actionCase === "userMessageAction" ? [text] : []),
910
+ ...(actionCase === "userMessageAction" ? [actionText] : []),
843
911
  ...mcpToolDefs.map(modelVisibleToolText),
844
912
  ];
845
913
  return {