@bitkyc08/opencodex 2.7.35 → 2.7.36
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/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +4 -2
- package/README.ru.md +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/ocx.mjs +52 -0
- package/gui/dist/assets/index-BpX-hoSd.css +1 -0
- package/gui/dist/assets/index-ZmFopEYw.js +52 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/cursor/cursor-errors.ts +38 -1
- package/src/adapters/cursor/discovery.ts +1 -0
- package/src/adapters/cursor/effort-map.ts +1 -0
- package/src/adapters/cursor/live-models.ts +22 -5
- package/src/adapters/cursor/live-transport.ts +82 -7
- package/src/adapters/cursor/transport.ts +2 -0
- package/src/adapters/cursor.ts +5 -2
- package/src/adapters/openai-responses.ts +64 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/help.ts +10 -0
- package/src/cli/index.ts +88 -9
- package/src/cli/internal-dispatch.ts +20 -0
- package/src/cli/status.ts +15 -4
- package/src/cli/tray-proxy.ts +52 -0
- package/src/codex/auth-api.ts +46 -5
- package/src/codex/autostart-health.ts +149 -0
- package/src/codex/catalog/aggregation.ts +268 -0
- package/src/codex/catalog/bundled.ts +188 -0
- package/src/codex/catalog/effort.ts +263 -0
- package/src/codex/catalog/metadata.ts +176 -0
- package/src/codex/catalog/parsing.ts +399 -0
- package/src/codex/catalog/provider-fetch.ts +609 -0
- package/src/codex/catalog/sync.ts +540 -0
- package/src/codex/catalog.ts +11 -2426
- package/src/codex/inject.ts +165 -3
- package/src/codex/shim.ts +141 -8
- package/src/codex/sync.ts +17 -2
- package/src/config.ts +23 -0
- package/src/lib/errors.ts +11 -0
- package/src/providers/antigravity-models.ts +33 -0
- package/src/providers/kiro-models.ts +2 -0
- package/src/providers/registry.ts +2 -2
- package/src/responses/state.ts +69 -6
- package/src/server/auth-cors.ts +3 -0
- package/src/server/management/agent-settings-routes.ts +536 -0
- package/src/server/management/combo-routes.ts +210 -0
- package/src/server/management/config-routes.ts +302 -0
- package/src/server/management/context.ts +21 -0
- package/src/server/management/logs-usage-routes.ts +176 -0
- package/src/server/management/model-routes.ts +253 -0
- package/src/server/management/oauth-account-routes.ts +301 -0
- package/src/server/management/provider-routes.ts +408 -0
- package/src/server/management/shared.ts +186 -0
- package/src/server/management-api.ts +23 -1806
- package/src/server/responses/collaboration.ts +300 -0
- package/src/server/responses/compact.ts +342 -0
- package/src/server/responses/core.ts +1498 -0
- package/src/server/responses/encrypted-payload.ts +231 -0
- package/src/server/responses/fetch-helpers.ts +157 -0
- package/src/server/responses.ts +9 -2172
- package/src/server/startup-action-control.ts +41 -0
- package/src/server/startup-health-cache.ts +100 -0
- package/src/server/windows-tray-control.ts +41 -0
- package/src/service.ts +171 -19
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +290 -0
- package/src/tray/windows.ts +628 -0
- package/src/types.ts +5 -0
- package/src/update/index.ts +43 -0
- package/src/update/job.ts +46 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +9 -2
- package/src/usage/summary.ts +42 -7
- package/gui/dist/assets/index-BunUANVE.js +0 -52
- package/gui/dist/assets/index-Sg-7L_oZ.css +0 -1
package/gui/dist/index.html
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-ZmFopEYw.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BpX-hoSd.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -47,6 +47,34 @@ export function isCursorInvalidArgumentError(value: unknown): boolean {
|
|
|
47
47
|
return message.includes("invalid_argument");
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
const QUOTA_RATE_CUES = ["too many requests", "quota", "rate limit", "rate-limit", "throttl"];
|
|
51
|
+
const REQUEST_TOO_LARGE_PATTERNS: (string | RegExp)[] = [
|
|
52
|
+
"tool catalog too large",
|
|
53
|
+
"tool registration too large",
|
|
54
|
+
"too many tools",
|
|
55
|
+
"message too large",
|
|
56
|
+
"payload too large",
|
|
57
|
+
"request too large",
|
|
58
|
+
// Size cue required somewhere: a bare "request exceeds ... limit" (concurrency/quota
|
|
59
|
+
// shape) must NOT match, but "request body/size exceeds ... limit" and
|
|
60
|
+
// "request exceeds maximum allowed size" are deterministic overflow (WP3 r1/r2).
|
|
61
|
+
/request exceeds .*size/,
|
|
62
|
+
/request (?:body|size) exceeds .*(?:size|limit)/,
|
|
63
|
+
"maximum allowed size",
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* True when a resource_exhausted detail names a request-size overflow rather than quota.
|
|
68
|
+
* Quota/rate cues are rejected FIRST: "resource_exhausted while loading tool catalog: quota
|
|
69
|
+
* exhausted" is rate limiting, not a too-large request, even though it mentions the catalog.
|
|
70
|
+
*/
|
|
71
|
+
export function isCursorRequestTooLargeDetail(lowerMessage: string): boolean {
|
|
72
|
+
if (QUOTA_RATE_CUES.some(cue => lowerMessage.includes(cue))) return false;
|
|
73
|
+
return REQUEST_TOO_LARGE_PATTERNS.some(pattern =>
|
|
74
|
+
typeof pattern === "string" ? lowerMessage.includes(pattern) : pattern.test(lowerMessage),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
50
78
|
/**
|
|
51
79
|
* Classify a Cursor transport/Connect/gRPC error message into an actionable category.
|
|
52
80
|
* The returned prefix string is recognized by `src/lib/errors.ts` `classifyError` keywords,
|
|
@@ -60,7 +88,16 @@ export function classifyCursorError(message: string): string {
|
|
|
60
88
|
if (
|
|
61
89
|
lower.includes("resource_exhausted") ||
|
|
62
90
|
lower.includes("resource exhausted")
|
|
63
|
-
)
|
|
91
|
+
) {
|
|
92
|
+
// gRPC RESOURCE_EXHAUSTED is quota/rate exhaustion unless the detail names a
|
|
93
|
+
// request-size overflow (tool catalog/registration). Only the latter is a
|
|
94
|
+
// client-fixable 400; everything else surfaces as a 429 so Codex backs off
|
|
95
|
+
// instead of hammering retries (live evidence: 6x 400 retry storm, devlog
|
|
96
|
+
// 260723_cursor_context_continuity/000_plan.md).
|
|
97
|
+
return isCursorRequestTooLargeDetail(lower)
|
|
98
|
+
? "Cursor resource limit exceeded"
|
|
99
|
+
: "Cursor rate limit exceeded";
|
|
100
|
+
}
|
|
64
101
|
|
|
65
102
|
if (
|
|
66
103
|
lower.includes("rate limit") ||
|
|
@@ -182,6 +182,7 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM
|
|
|
182
182
|
// opus-4-7-fast: effort-suffix tiers unverified -> no tier picker; sent bare like live-only ids.
|
|
183
183
|
{ id: "claude-opus-4-7-fast", contextWindow: CONTEXT_200K },
|
|
184
184
|
{ id: "claude-opus-4-8", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
185
|
+
{ id: "claude-opus-5", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
185
186
|
{ id: "claude-fable-5", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
186
187
|
|
|
187
188
|
{ id: "composer-1", contextWindow: CONTEXT_200K },
|
|
@@ -24,6 +24,7 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
|
|
|
24
24
|
"claude-fable-5": ["low", "medium", "high", "xhigh", "max"],
|
|
25
25
|
"claude-opus-4-7": ["low", "medium", "high", "xhigh", "max"],
|
|
26
26
|
"claude-opus-4-8": ["low", "medium", "high", "xhigh", "max"],
|
|
27
|
+
"claude-opus-5": ["low", "medium", "high", "xhigh", "max"],
|
|
27
28
|
"claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"],
|
|
28
29
|
"glm-5.2": ["high", "max"],
|
|
29
30
|
// GetUsableModels (2026-07-09) lists grok-4.5-{medium,high,xhigh} and grok-4.5-fast-{medium,high,xhigh};
|
|
@@ -28,9 +28,26 @@ export interface CursorUsableModelsOptions {
|
|
|
28
28
|
|
|
29
29
|
export type CursorUsableModelsResult =
|
|
30
30
|
| { ok: true; models: string[] }
|
|
31
|
-
| { ok: false; error: "auth" | "http" | "timeout" | "decode" | "empty"; detail?: string };
|
|
31
|
+
| { ok: false; error: "auth" | "http" | "transport" | "timeout" | "decode" | "empty"; detail?: string };
|
|
32
32
|
|
|
33
|
+
const RETRYABLE_DISCOVERY_ERRORS = new Set(["timeout", "transport"]);
|
|
34
|
+
const DISCOVERY_RETRY_TIMEOUT_MS = 3_000;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Live discovery with ONE bounded retry for transient pre-response failures (fresh HTTP/2
|
|
38
|
+
* session each attempt). Completed non-2xx responses ("http"), auth, decode, and empty are
|
|
39
|
+
* deterministic and never retried. The retry attempt's deadline is capped so a cache-miss
|
|
40
|
+
* catalog poll cannot stall much past the primary timeout (~11.5s worst case, accepted in
|
|
41
|
+
* devlog 260723_cursor_context_continuity/030).
|
|
42
|
+
*/
|
|
33
43
|
export async function fetchCursorUsableModels(opts: CursorUsableModelsOptions): Promise<CursorUsableModelsResult> {
|
|
44
|
+
const first = await fetchCursorUsableModelsOnce(opts);
|
|
45
|
+
if (first.ok || !RETRYABLE_DISCOVERY_ERRORS.has(first.error)) return first;
|
|
46
|
+
await new Promise(resolve => setTimeout(resolve, 250 + Math.floor(Math.random() * 250)));
|
|
47
|
+
return fetchCursorUsableModelsOnce({ ...opts, timeoutMs: Math.min(opts.timeoutMs ?? 8000, DISCOVERY_RETRY_TIMEOUT_MS) });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function fetchCursorUsableModelsOnce(opts: CursorUsableModelsOptions): Promise<CursorUsableModelsResult> {
|
|
34
51
|
const baseUrl = (opts.baseUrl ?? "https://api2.cursor.sh").replace(/\/+$/, "");
|
|
35
52
|
const timeoutMs = opts.timeoutMs ?? 8000;
|
|
36
53
|
|
|
@@ -46,7 +63,7 @@ export async function fetchCursorUsableModels(opts: CursorUsableModelsOptions):
|
|
|
46
63
|
try {
|
|
47
64
|
client = http2.connect(baseUrl);
|
|
48
65
|
} catch {
|
|
49
|
-
return finish({ ok: false, error: "
|
|
66
|
+
return finish({ ok: false, error: "transport", detail: "HTTP/2 connection setup failed" });
|
|
50
67
|
}
|
|
51
68
|
|
|
52
69
|
const timer = setTimeout(() => {
|
|
@@ -59,7 +76,7 @@ export async function fetchCursorUsableModels(opts: CursorUsableModelsOptions):
|
|
|
59
76
|
finish(value);
|
|
60
77
|
};
|
|
61
78
|
|
|
62
|
-
client.on("error", () => close({ ok: false, error: "
|
|
79
|
+
client.on("error", () => close({ ok: false, error: "transport", detail: "HTTP/2 session failed" }));
|
|
63
80
|
|
|
64
81
|
let req: http2.ClientHttp2Stream;
|
|
65
82
|
try {
|
|
@@ -75,7 +92,7 @@ export async function fetchCursorUsableModels(opts: CursorUsableModelsOptions):
|
|
|
75
92
|
"x-session-id": crypto.randomUUID(),
|
|
76
93
|
});
|
|
77
94
|
} catch {
|
|
78
|
-
return close({ ok: false, error: "
|
|
95
|
+
return close({ ok: false, error: "transport", detail: "HTTP/2 request setup failed" });
|
|
79
96
|
}
|
|
80
97
|
|
|
81
98
|
let status = 0;
|
|
@@ -84,7 +101,7 @@ export async function fetchCursorUsableModels(opts: CursorUsableModelsOptions):
|
|
|
84
101
|
status = Number(headers[":status"] ?? 0);
|
|
85
102
|
});
|
|
86
103
|
req.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
87
|
-
req.on("error", () => close({ ok: false, error: "
|
|
104
|
+
req.on("error", () => close({ ok: false, error: "transport", detail: "HTTP/2 request failed" }));
|
|
88
105
|
req.on("end", () => {
|
|
89
106
|
if (status === 401 || status === 403) {
|
|
90
107
|
return close({ ok: false, error: "auth", detail: `HTTP ${status}` });
|
|
@@ -64,12 +64,60 @@ const CURSOR_RUN_PATH = "/agent.v1.AgentService/Run";
|
|
|
64
64
|
const CURSOR_CLIENT_VERSION = "cli-2026.07.08-0c04a8a";
|
|
65
65
|
const HEARTBEAT_MS = 5_000;
|
|
66
66
|
const CURSOR_FIRST_FRAME_TIMEOUT_MS = 30_000;
|
|
67
|
+
const CURSOR_TIMEOUT_DESTROY_GRACE_MS = 1_000;
|
|
67
68
|
const CLIENT_TOOL_FINALIZE_GRACE_MS = 50;
|
|
68
69
|
const GENERIC_TOOL_COUNT_MIN_FINALIZE_GRACE_MS = 750;
|
|
69
70
|
const GENERIC_TOOL_COUNT_MAX_FINALIZE_GRACE_MS = 1_800;
|
|
70
71
|
const GENERIC_TOOL_COUNT_PER_TOOL_GRACE_MS = 125;
|
|
71
72
|
const cursorContextUsageTracker = createCursorContextUsageTracker();
|
|
72
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Single-shot terminal settlement for one Cursor turn: whichever of fail/finish wins first owns
|
|
76
|
+
* the terminal; later calls are no-ops. Prevents double-terminal mutation when multiple sources
|
|
77
|
+
* race (stream error + session error, end + late session error, timeout + destroy error).
|
|
78
|
+
* Exported for direct unit testing — the callbacks are otherwise private to run()/open().
|
|
79
|
+
*/
|
|
80
|
+
export function createTerminalSettler(hooks: {
|
|
81
|
+
fail: (error: Error) => void;
|
|
82
|
+
finish: () => void;
|
|
83
|
+
clearTimer: () => void;
|
|
84
|
+
}): { settleFail: (error: Error) => void; settleFinish: () => void; settled: () => boolean } {
|
|
85
|
+
let settled = false;
|
|
86
|
+
return {
|
|
87
|
+
settleFail(error) {
|
|
88
|
+
if (settled) return;
|
|
89
|
+
settled = true;
|
|
90
|
+
hooks.clearTimer();
|
|
91
|
+
hooks.fail(error);
|
|
92
|
+
},
|
|
93
|
+
settleFinish() {
|
|
94
|
+
if (settled) return;
|
|
95
|
+
settled = true;
|
|
96
|
+
hooks.clearTimer();
|
|
97
|
+
hooks.finish();
|
|
98
|
+
},
|
|
99
|
+
settled: () => settled,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Arm the post-close destroy fallback for a timed-out turn: close() waits for in-flight frames,
|
|
105
|
+
* but a dead socket can ignore it, leaving a stalled TLS session past the timeout. Exported for
|
|
106
|
+
* unit testing with fakes. The timer is unref'd so it never holds the process open.
|
|
107
|
+
*/
|
|
108
|
+
export function armTimeoutDestroyFallback(
|
|
109
|
+
stream: { destroyed: boolean; destroy: () => void },
|
|
110
|
+
session: { destroyed: boolean; destroy: () => void },
|
|
111
|
+
graceMs: number,
|
|
112
|
+
): ReturnType<typeof setTimeout> {
|
|
113
|
+
const timer = setTimeout(() => {
|
|
114
|
+
try { if (!stream.destroyed) stream.destroy(); } catch { /* gone */ }
|
|
115
|
+
try { if (!session.destroyed) session.destroy(); } catch { /* gone */ }
|
|
116
|
+
}, graceMs);
|
|
117
|
+
timer.unref?.();
|
|
118
|
+
return timer;
|
|
119
|
+
}
|
|
120
|
+
|
|
73
121
|
/** Carry context-usage totals across conversation-id rotation for external-model replay. */
|
|
74
122
|
export function rekeyCursorContextUsage(fromConversationId: string, toConversationId: string): void {
|
|
75
123
|
cursorContextUsageTracker.rekey(fromConversationId, toConversationId);
|
|
@@ -616,10 +664,15 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
616
664
|
"x-session-id": this.sessionId,
|
|
617
665
|
});
|
|
618
666
|
|
|
619
|
-
// Single
|
|
620
|
-
//
|
|
667
|
+
// Single-shot terminal owner for this turn (createTerminalSettler): stream error, session
|
|
668
|
+
// error, trailers, end, abort, and the first-frame timeout all race into it, and only the
|
|
669
|
+
// first wins. The first-frame timer is cleared by any settlement so it can never leak.
|
|
670
|
+
const settler = createTerminalSettler({
|
|
671
|
+
fail,
|
|
672
|
+
finish,
|
|
673
|
+
clearTimer: () => this.clearFirstFrameTimer(),
|
|
674
|
+
});
|
|
621
675
|
const failAndClear = (error: Error) => {
|
|
622
|
-
this.clearFirstFrameTimer();
|
|
623
676
|
if (this.expectedClose) {
|
|
624
677
|
// We already emitted a terminal `done` and cancelled the run (client-tool suspension). The
|
|
625
678
|
// RST_STREAM CANCEL surfaces here as a stream error/abort; it is expected, not a failure.
|
|
@@ -629,19 +682,33 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
629
682
|
framesReceived: this.framesReceived,
|
|
630
683
|
elapsedMs: Date.now() - this.turnStartedAt,
|
|
631
684
|
});
|
|
632
|
-
|
|
685
|
+
settler.settleFinish();
|
|
633
686
|
return;
|
|
634
687
|
}
|
|
635
|
-
|
|
688
|
+
settler.settleFail(error);
|
|
636
689
|
};
|
|
637
690
|
const session = this.session;
|
|
638
691
|
const stream = this.stream;
|
|
692
|
+
// Session-level errors (TLS/socket/GOAWAY) do not always propagate to the stream listener;
|
|
693
|
+
// without this handler they could bypass orderly failure reporting entirely.
|
|
694
|
+
session.on("error", err => {
|
|
695
|
+
const realErr = err instanceof Error ? err : new Error(String(err));
|
|
696
|
+
debugProviderDiagnostic("cursor", "session-error", {
|
|
697
|
+
code: String((realErr as { code?: unknown }).code ?? ""),
|
|
698
|
+
message: redactCursorForLog(realErr.message),
|
|
699
|
+
elapsedMs: Date.now() - this.turnStartedAt,
|
|
700
|
+
});
|
|
701
|
+
failAndClear(realErr);
|
|
702
|
+
});
|
|
639
703
|
this.firstFrameTimer = setTimeout(() => {
|
|
640
704
|
this.firstFrameTimer = undefined;
|
|
641
705
|
debugProviderDiagnostic("cursor", "first-frame-timeout", { timeoutMs: this.input.firstFrameTimeoutMs ?? CURSOR_FIRST_FRAME_TIMEOUT_MS });
|
|
642
706
|
try { stream.close(); } catch { /* already closing */ }
|
|
643
707
|
try { session.close(); } catch { /* already closing */ }
|
|
644
|
-
|
|
708
|
+
// close() waits for in-flight frames; a dead socket can ignore it — force-destroy shortly
|
|
709
|
+
// after so a stalled TLS session cannot linger past the timeout.
|
|
710
|
+
armTimeoutDestroyFallback(stream, session, this.input.timeoutDestroyGraceMs ?? CURSOR_TIMEOUT_DESTROY_GRACE_MS);
|
|
711
|
+
settler.settleFail(new Error("Cursor transport timed out before first response"));
|
|
645
712
|
}, this.input.firstFrameTimeoutMs ?? CURSOR_FIRST_FRAME_TIMEOUT_MS);
|
|
646
713
|
|
|
647
714
|
let pending: Uint8Array<ArrayBufferLike> = new Uint8Array();
|
|
@@ -712,7 +779,15 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
712
779
|
expectedClose: this.expectedClose,
|
|
713
780
|
elapsedMs: Date.now() - this.turnStartedAt,
|
|
714
781
|
});
|
|
715
|
-
|
|
782
|
+
// A zero-frame end without an expected close is an unexpected EOF (peer dropped the
|
|
783
|
+
// connection before any response frame) — surfacing it as success would silently
|
|
784
|
+
// swallow the turn (WP4 review blocker 1). With frames, the protobuf event state
|
|
785
|
+
// owns terminal semantics as before.
|
|
786
|
+
if (this.framesReceived === 0 && !this.expectedClose) {
|
|
787
|
+
settler.settleFail(new Error("Cursor stream ended before any response frame (unexpected EOF)"));
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
settler.settleFinish();
|
|
716
791
|
});
|
|
717
792
|
|
|
718
793
|
signal?.addEventListener("abort", () => {
|
|
@@ -18,6 +18,8 @@ export interface CursorTransportFactoryInput {
|
|
|
18
18
|
headers?: Headers;
|
|
19
19
|
/** Pre-first-frame deadline (dial + first server frame). Defaults to 30s when omitted. */
|
|
20
20
|
firstFrameTimeoutMs?: number;
|
|
21
|
+
/** Grace (ms) between close() and the force-destroy fallback after a first-frame timeout. Defaults to 1s. */
|
|
22
|
+
timeoutDestroyGraceMs?: number;
|
|
21
23
|
/**
|
|
22
24
|
* Grace window (ms) before a drained client-tool turn is finalized, so a sibling tool call
|
|
23
25
|
* announced in a later receive chunk can revoke a premature finalize. Defaults to 50ms.
|
package/src/adapters/cursor.ts
CHANGED
|
@@ -34,6 +34,8 @@ const CURSOR_TRANSPORT_DISABLED_MESSAGE = [
|
|
|
34
34
|
export interface CursorAdapterDeps {
|
|
35
35
|
createTransport?: CursorTransportFactory;
|
|
36
36
|
kv?: CursorKvStore;
|
|
37
|
+
/** Test seam: observe/replace context-usage rekeying on conversation-id rotation. */
|
|
38
|
+
rekeyContextUsage?: (fromConversationId: string, toConversationId: string) => void;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
function safeCursorTransportError(err: unknown): string {
|
|
@@ -74,13 +76,14 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
74
76
|
try {
|
|
75
77
|
const makeTransport = deps.createTransport ?? createLiveCursorTransport;
|
|
76
78
|
const kv = deps.kv ?? createCursorKvStore();
|
|
79
|
+
const rekeyContextUsage = deps.rekeyContextUsage ?? rekeyCursorContextUsage;
|
|
77
80
|
_parsed._cursorConversationId ??= generatedCursorConversationId();
|
|
78
81
|
const previousConversationId = _parsed._cursorConversationId;
|
|
79
82
|
let request = createCursorRequest(_parsed);
|
|
80
83
|
// Keep remembered conversation id in sync when the request builder mints a fresh id
|
|
81
84
|
// for external-model tool-result continuations (stateless replay).
|
|
82
85
|
if (request.conversationId !== previousConversationId) {
|
|
83
|
-
|
|
86
|
+
rekeyContextUsage(previousConversationId, request.conversationId);
|
|
84
87
|
}
|
|
85
88
|
_parsed._cursorConversationId = request.conversationId;
|
|
86
89
|
let emittedOutput = false;
|
|
@@ -133,7 +136,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
133
136
|
const failedConversationId = request.conversationId;
|
|
134
137
|
_parsed._cursorConversationId = undefined;
|
|
135
138
|
request = createCursorRequest(_parsed, { forceFreshConversation: true });
|
|
136
|
-
|
|
139
|
+
rekeyContextUsage(failedConversationId, request.conversationId);
|
|
137
140
|
_parsed._cursorConversationId = request.conversationId;
|
|
138
141
|
await runOnce(request);
|
|
139
142
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { IncomingMeta, ProviderAdapter } from "./base";
|
|
2
2
|
import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../types";
|
|
3
|
+
import { catalogModelSupportsReasoningSummaries } from "../codex/catalog";
|
|
3
4
|
import { decodeCompactionSummary, SUMMARY_PREFIX } from "../responses/compaction";
|
|
4
5
|
import { OCX_REASONING_PREFIX } from "../responses/reasoning-envelope";
|
|
6
|
+
import { modelRecordValue } from "../reasoning-effort";
|
|
5
7
|
|
|
6
8
|
// Headers relayed verbatim from the caller in OAuth-passthrough ("forward") mode.
|
|
7
9
|
// Exported so the web-search sidecar reuses the exact same forwarded-auth set for its ChatGPT call.
|
|
@@ -52,6 +54,19 @@ export function sanitizeReasoningInputContent(body: unknown): unknown {
|
|
|
52
54
|
return changed ? { ...raw, input } : body;
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
function stripUnsupportedReasoningSummaryDelivery(body: unknown, modelId: string): unknown {
|
|
58
|
+
if (catalogModelSupportsReasoningSummaries(modelId) !== false) return body;
|
|
59
|
+
if (!isPlainObject(body) || !isPlainObject(body.stream_options)) return body;
|
|
60
|
+
if (!("reasoning_summary_delivery" in body.stream_options)) return body;
|
|
61
|
+
|
|
62
|
+
const streamOptions = { ...body.stream_options };
|
|
63
|
+
delete streamOptions.reasoning_summary_delivery;
|
|
64
|
+
const next = { ...body };
|
|
65
|
+
if (Object.keys(streamOptions).length > 0) next.stream_options = streamOptions;
|
|
66
|
+
else delete next.stream_options;
|
|
67
|
+
return next;
|
|
68
|
+
}
|
|
69
|
+
|
|
55
70
|
function stripInvalidItemIds(body: unknown): unknown {
|
|
56
71
|
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
57
72
|
|
|
@@ -158,6 +173,49 @@ function stripUnsupportedReasoningParams(body: unknown): unknown {
|
|
|
158
173
|
return { ...body, reasoning: Object.keys(rest).length > 0 ? rest : undefined };
|
|
159
174
|
}
|
|
160
175
|
|
|
176
|
+
/**
|
|
177
|
+
* A false model capability prevents Codex from emitting summary fields after the catalog refresh.
|
|
178
|
+
* Strip them here as well so an already-running client with a stale catalog cannot keep sending an
|
|
179
|
+
* upstream-rejected `reasoning_summary_delivery` value (issue #323).
|
|
180
|
+
*/
|
|
181
|
+
function stripDisabledReasoningSummaries(
|
|
182
|
+
body: unknown,
|
|
183
|
+
provider: OcxProviderConfig,
|
|
184
|
+
modelId: string,
|
|
185
|
+
): unknown {
|
|
186
|
+
if (modelRecordValue(provider.modelSupportsReasoningSummaries, modelId) !== false || !isPlainObject(body)) {
|
|
187
|
+
return body;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let changed = false;
|
|
191
|
+
let streamOptions = body.stream_options;
|
|
192
|
+
if (isPlainObject(streamOptions) && Object.hasOwn(streamOptions, "reasoning_summary_delivery")) {
|
|
193
|
+
const { reasoning_summary_delivery: _delivery, ...rest } = streamOptions;
|
|
194
|
+
streamOptions = rest;
|
|
195
|
+
changed = true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let reasoning = body.reasoning;
|
|
199
|
+
if (isPlainObject(reasoning)) {
|
|
200
|
+
const { summary: _summary, generate_summary: _generateSummary, ...rest } = reasoning;
|
|
201
|
+
if (_summary !== undefined || _generateSummary !== undefined) {
|
|
202
|
+
reasoning = rest;
|
|
203
|
+
changed = true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!changed) return body;
|
|
208
|
+
return {
|
|
209
|
+
...body,
|
|
210
|
+
...(isPlainObject(streamOptions) && Object.keys(streamOptions).length > 0
|
|
211
|
+
? { stream_options: streamOptions }
|
|
212
|
+
: { stream_options: undefined }),
|
|
213
|
+
...(isPlainObject(reasoning) && Object.keys(reasoning).length > 0
|
|
214
|
+
? { reasoning }
|
|
215
|
+
: { reasoning: undefined }),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
161
219
|
/**
|
|
162
220
|
* Comprehensive Spark compatibility layer. codex-rs emits five tool types (function,
|
|
163
221
|
* namespace, tool_search, web_search, custom) plus extensions (defer_loading,
|
|
@@ -459,11 +517,16 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
459
517
|
);
|
|
460
518
|
if (forward) outBody = repairOrphanedInputItems(outBody, unexpandedMiss);
|
|
461
519
|
else outBody = stripConflictingHostedTools(outBody);
|
|
520
|
+
outBody = stripUnsupportedReasoningSummaryDelivery(outBody, parsed.modelId);
|
|
462
521
|
return {
|
|
463
522
|
url,
|
|
464
523
|
method: "POST",
|
|
465
524
|
headers,
|
|
466
|
-
body: JSON.stringify(
|
|
525
|
+
body: JSON.stringify(stripDisabledReasoningSummaries(
|
|
526
|
+
stripSparkCompatibility(stripUnsupportedReasoningParams(stripItemIdsWhenUnstored(stripInvalidItemIds(stripUnsupportedHostedTools(sanitizeReasoningInputContent(scrubOcxCompactionItems(outBody))))))),
|
|
527
|
+
provider,
|
|
528
|
+
parsed.modelId,
|
|
529
|
+
)),
|
|
467
530
|
};
|
|
468
531
|
},
|
|
469
532
|
|
package/src/cli/doctor.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { resolveCodexHomeDir as resolveCodexHomeDirImpl, isWslRuntime, listWslWi
|
|
|
16
16
|
import { findCodexOnPath, isWindowsInteropDir } from "../codex/shim";
|
|
17
17
|
import { countPendingOpencodexHistory } from "../codex/history-provider";
|
|
18
18
|
import { collectProjectCodexConfigWarnings, formatProjectCodexConfigWarningsForDoctor } from "../codex/project-config-warnings";
|
|
19
|
+
import { collectStartupHealth, startupHealthSummary } from "../codex/autostart-health";
|
|
19
20
|
export { resolveCodexHomeDir } from "../codex/home";
|
|
20
21
|
|
|
21
22
|
const WHAM_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
@@ -331,6 +332,11 @@ export async function runDoctor(): Promise<void> {
|
|
|
331
332
|
console.log(` ${row.exists ? "ok " : "-- "} ${row.label}: ${row.path}${flags ? ` (${flags})` : ""}`);
|
|
332
333
|
}
|
|
333
334
|
|
|
335
|
+
const startup = collectStartupHealth(readConfigDiagnostics().config);
|
|
336
|
+
console.log("\nCodex restart safety");
|
|
337
|
+
console.log(` ${startup.rebootSafe ? "ok " : "!! "} ${startupHealthSummary(startup)}`);
|
|
338
|
+
console.log(` routing=${startup.routingKind}, service=${startup.serviceViable ? "viable" : startup.serviceInstalled ? "installed-but-unhealthy" : "absent"}, shim=${startup.shimHealthy ? "healthy" : startup.shimInstalled ? "stale" : "absent"}`);
|
|
339
|
+
|
|
334
340
|
const currentProxyEnv = collectProxyEnv();
|
|
335
341
|
const configuredProxy = collectConfiguredProxy();
|
|
336
342
|
const runningProxyEnv = collectRunningProxyEnv();
|
|
@@ -403,6 +409,10 @@ export async function runDoctor(): Promise<void> {
|
|
|
403
409
|
const hints: string[] = [];
|
|
404
410
|
const anyDrvfs = paths.some(p => detectFsType(p.path, mounts).isDrvfs || detectFsType(p.path, mounts).isMntDrive);
|
|
405
411
|
const noProxy = currentProxyEnv.every(p => !p.present) && !configuredProxy.present;
|
|
412
|
+
if (!startup.rebootSafe) {
|
|
413
|
+
const command = startup.recommendedCommand ?? startup.commands.restoreNative;
|
|
414
|
+
hints.push(`Codex is pinned to the local proxy without persistent startup protection. After restart, requests can reconnect indefinitely. Run '${command}'.`);
|
|
415
|
+
}
|
|
406
416
|
if (anyDrvfs) {
|
|
407
417
|
hints.push("State dir is on a Windows-mounted (/mnt) drive. Prefer the Linux home (~) under WSL for token/lock reliability.");
|
|
408
418
|
}
|
package/src/cli/help.ts
CHANGED
|
@@ -49,6 +49,15 @@ const helpEntries: Record<string, HelpEntry> = {
|
|
|
49
49
|
summary: "Auto-start the proxy when `codex` launches.",
|
|
50
50
|
details: ["Use `remove` as an alias for `uninstall`."],
|
|
51
51
|
},
|
|
52
|
+
tray: {
|
|
53
|
+
usage: "ocx tray <install|start|stop|status|uninstall|remove> [--json] [--no-start]",
|
|
54
|
+
summary: "Install and control the Windows status tray icon.",
|
|
55
|
+
details: [
|
|
56
|
+
"The tray starts at Windows login and provides one-click proxy controls.",
|
|
57
|
+
"Tray start/stop controls the icon only; use its menu to start or stop the proxy.",
|
|
58
|
+
"--no-start (install only) installs the tray without launching it immediately.",
|
|
59
|
+
],
|
|
60
|
+
},
|
|
52
61
|
ensure: { usage: "ocx ensure", summary: "Ensure the proxy is running and Codex config/cache are current." },
|
|
53
62
|
sync: { usage: "ocx sync", summary: "Fetch provider models and inject them into Codex config." },
|
|
54
63
|
"sync-cache": { usage: "ocx sync-cache", summary: "Refresh Codex's model cache from the active catalog." },
|
|
@@ -164,6 +173,7 @@ Usage:
|
|
|
164
173
|
ocx uninstall Remove service/shim/config and restore native Codex (alias: remove)
|
|
165
174
|
ocx service [sub] Run as a background service (default: install/update/start)
|
|
166
175
|
ocx codex-shim <sub> Auto-start proxy when \`codex\` launches (install|status|uninstall|remove)
|
|
176
|
+
ocx tray <sub> Windows status tray (install|start|stop|status|uninstall)
|
|
167
177
|
ocx ensure Ensure the proxy is running and Codex config/cache are current
|
|
168
178
|
ocx sync Fetch models from providers and inject into Codex config
|
|
169
179
|
ocx sync-cache Refresh Codex's model cache from the active catalog
|