@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.
- package/bin/ocx.mjs +59 -7
- package/gui/dist/assets/index-RL6b1bTV.js +102 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/base.ts +18 -0
- package/src/adapters/client-fingerprint.ts +0 -2
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/live-models.ts +117 -30
- package/src/adapters/cursor/live-transport.ts +137 -56
- package/src/adapters/cursor/native-exec-fs.ts +1 -1
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +0 -1
- package/src/adapters/cursor/protobuf-request.ts +89 -21
- package/src/adapters/cursor/tool-definitions.ts +18 -7
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport.ts +7 -0
- package/src/adapters/cursor.ts +2 -0
- package/src/adapters/google-http.ts +38 -10
- package/src/adapters/google.ts +23 -3
- package/src/adapters/openai-chat.ts +38 -13
- package/src/adapters/openai-responses.ts +129 -9
- package/src/adapters/registry.ts +30 -1
- package/src/bridge.ts +50 -12
- package/src/chat/inbound.ts +1 -0
- package/src/claude/agents-inject.ts +3 -2
- package/src/cli/dispatch.ts +19 -6
- package/src/cli/help.ts +2 -1
- package/src/cli/integrations.ts +35 -0
- package/src/cli/minimax.ts +8 -2
- package/src/cli/registry.ts +13 -2
- package/src/clients/config-export.ts +120 -1
- package/src/codex/account-store.ts +17 -1
- package/src/codex/auth-api.ts +51 -17
- package/src/codex/catalog/effort.ts +8 -5
- package/src/codex/catalog/provider-fetch.ts +39 -27
- package/src/codex/catalog/sync.ts +30 -8
- package/src/codex/inject.ts +39 -13
- package/src/codex/main-account.ts +29 -1
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +25 -0
- package/src/codex/prompt-journal.ts +6 -2
- package/src/codex/quota-rejection.ts +21 -7
- package/src/codex/refresh.ts +4 -2
- package/src/codex/sync.ts +106 -3
- package/src/codex/warmup.ts +187 -81
- package/src/config.ts +85 -46
- package/src/generated/compatibility-version.json +127 -83
- package/src/grok/inject.ts +1 -1
- package/src/images/loop.ts +1 -0
- package/src/integrations/registry.ts +7 -0
- package/src/lab/automation/config-persistence.ts +2 -2
- package/src/lab/automation/persistence.ts +2 -2
- package/src/lab/ledger/purge.ts +2 -2
- package/src/lab/subject/behavior-fingerprint.ts +2 -2
- package/src/lib/config-ownership.ts +5 -2
- package/src/lib/destination-policy.ts +18 -1
- package/src/lib/provider-outbound.ts +7 -0
- package/src/lib/redact.ts +11 -0
- package/src/lib/tool-argument-integers.ts +50 -6
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-retry.ts +2 -1
- package/src/lib/windows-atomic-replace.ts +155 -0
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/oauth/google-antigravity.ts +2 -2
- package/src/oauth/index.ts +60 -8
- package/src/providers/antigravity-models.ts +58 -4
- package/src/providers/command-code-efforts.ts +36 -9
- package/src/providers/context-cap.ts +9 -0
- package/src/providers/derive.ts +4 -0
- package/src/providers/fastwire.ts +453 -0
- package/src/providers/registry.ts +21 -1
- package/src/providers/service-tier.ts +173 -89
- package/src/responses/parser.ts +25 -16
- package/src/responses/reasoning-replay-cache.ts +30 -0
- package/src/responses/thought-signature-replay.ts +74 -4
- package/src/router.ts +6 -0
- package/src/routing/compatibility/behavior.ts +27 -16
- package/src/server/index.ts +10 -1
- package/src/server/management/oauth-account-routes.ts +10 -2
- package/src/server/management/shared.ts +1 -1
- package/src/server/management/system-routes.ts +15 -0
- package/src/server/request-log.ts +57 -3
- package/src/server/responses/agent-task-recovery.ts +6 -1
- package/src/server/responses/core.ts +209 -40
- package/src/server/responses/fetch-helpers.ts +15 -36
- package/src/server/responses/responses-field-backfill.ts +173 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +171 -0
- package/src/service.ts +38 -36
- package/src/storage/cleanup.ts +2 -2
- package/src/tray/windows.ts +3 -2
- package/src/types.ts +101 -14
- package/src/update/job.ts +9 -18
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +259 -0
- package/src/usage/cost.ts +34 -5
- package/src/usage/log.ts +74 -4
- package/src/vision/anthropic-describe.ts +10 -6
- package/src/web-search/anthropic-executor.ts +8 -6
- 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
|
|
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.
|
|
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.
|
|
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
|
-
)
|
|
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.
|
|
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
|
-
|
|
725
|
-
this.
|
|
726
|
-
}
|
|
727
|
-
|
|
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
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
"connect
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|
-
|
|
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
|
|
865
|
-
try { session
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1169
|
+
this.writeConnectFrame(encodeConnectFrame(encodedRequest));
|
|
1089
1170
|
this.heartbeat = setInterval(() => {
|
|
1090
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
179
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
420
|
-
|
|
421
|
-
|
|
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: ${
|
|
507
|
+
`is_error: ${normalized.isError}`,
|
|
480
508
|
"output:",
|
|
481
|
-
|
|
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,
|
|
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
|
-
|
|
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${
|
|
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
|
-
|
|
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" ? [
|
|
910
|
+
...(actionCase === "userMessageAction" ? [actionText] : []),
|
|
843
911
|
...mcpToolDefs.map(modelVisibleToolText),
|
|
844
912
|
];
|
|
845
913
|
return {
|