@bitkyc08/opencodex 2.29.0 → 2.31.0-preview.20260822
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.md +5 -5
- package/gui/dist/assets/index-DyWYnr-t.js +102 -0
- package/gui/dist/index.html +1 -1
- package/package.json +3 -3
- package/src/adapters/cursor/cursor-errors.ts +65 -6
- package/src/adapters/cursor/discovery.ts +29 -2
- package/src/adapters/cursor/effort-map.ts +6 -0
- package/src/adapters/cursor/h2-pool.ts +123 -0
- package/src/adapters/cursor/images.ts +704 -0
- package/src/adapters/cursor/live-models.ts +21 -26
- package/src/adapters/cursor/live-transport.ts +239 -8
- package/src/adapters/cursor/native-exec-common.ts +17 -0
- package/src/adapters/cursor/native-exec.ts +9 -4
- package/src/adapters/cursor/protobuf-events.ts +5 -1
- package/src/adapters/cursor/protobuf-request.ts +46 -9
- package/src/adapters/cursor/request-builder.ts +29 -14
- package/src/adapters/cursor/tool-definitions.ts +20 -0
- package/src/adapters/cursor/transport.ts +10 -0
- package/src/adapters/cursor/types.ts +8 -1
- package/src/adapters/cursor.ts +23 -5
- package/src/adapters/google.ts +16 -3
- package/src/adapters/openai-responses.ts +66 -20
- package/src/adapters/xai-web-search.ts +185 -0
- package/src/cli/agent.ts +2 -1
- package/src/cli/dispatch.ts +2 -2
- package/src/cli/doctor.ts +89 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/registry.ts +7 -2
- package/src/codex/auth-context.ts +41 -2
- package/src/codex/catalog/effort.ts +1 -1
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-fetch.ts +20 -5
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/features.ts +58 -0
- package/src/codex/inject-coordination.ts +39 -6
- package/src/codex/transition-state.ts +12 -12
- package/src/generated/compatibility-version.json +86 -58
- package/src/lib/bun-stream-caps.ts +7 -4
- package/src/lib/errors.ts +8 -2
- package/src/oauth/cursor.ts +21 -0
- package/src/providers/command-code-efforts.ts +7 -0
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/derive.ts +3 -0
- package/src/providers/fastwire.ts +12 -1
- package/src/providers/openai-sidecar.ts +1 -0
- package/src/providers/quota.ts +98 -25
- package/src/providers/registry.ts +115 -10
- package/src/providers/service-tier.ts +22 -7
- package/src/responses/custom-tool-compat.ts +24 -8
- package/src/responses/namespace-tool-compat.ts +2 -3
- package/src/router.ts +3 -0
- package/src/server/chat-completions.ts +4 -0
- package/src/server/chat-native.ts +20 -0
- package/src/server/management/agent-settings-routes.ts +16 -5
- package/src/server/management/config-routes.ts +25 -5
- package/src/server/management/vision-sidecar-options.ts +54 -19
- package/src/server/responses/compact.ts +1 -2
- package/src/server/responses/core.ts +54 -13
- package/src/service.ts +122 -14
- package/src/types/config.ts +9 -3
- package/src/types/provider.ts +6 -0
- package/src/usage/cost.ts +52 -38
- package/src/usage/expected-prices.ts +79 -9
- package/src/vision/backends.ts +97 -0
- package/src/vision/eligibility.ts +43 -22
- package/src/vision/index.ts +73 -5
- package/src/vision/routed-describe.ts +175 -0
- package/gui/dist/assets/index-BNESwCzn.js +0 -102
package/gui/dist/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-DyWYnr-t.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-CH7ncHCC.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitkyc08/opencodex",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.31.0-preview.20260822",
|
|
4
4
|
"description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./bin/package-main.mjs",
|
|
@@ -62,11 +62,11 @@
|
|
|
62
62
|
"@bufbuild/protobuf": "^2.14.0",
|
|
63
63
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
64
64
|
"@napi-rs/keyring": "1.3.0",
|
|
65
|
-
"bun": "1.
|
|
65
|
+
"bun": "1.4.0",
|
|
66
66
|
"zod": "4.4.3"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
|
-
"@types/bun": "1.
|
|
69
|
+
"@types/bun": "1.4.0",
|
|
70
70
|
"typescript": "7.0.2"
|
|
71
71
|
},
|
|
72
72
|
"overrides": {
|
|
@@ -112,6 +112,58 @@ export function isCursorInvalidArgumentError(value: unknown): boolean {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
const QUOTA_RATE_CUES = ["too many requests", "quota", "rate limit", "rate-limit", "throttl"];
|
|
115
|
+
/**
|
|
116
|
+
* A bare `resource_exhausted` end-stream with no detail beyond a generic error wrapper
|
|
117
|
+
* ("Error" or empty tail) and zero tokens billed is the shape Cursor's backend emits when
|
|
118
|
+
* the request payload exceeded its context window — not when quota ran out (senpi #1009,
|
|
119
|
+
* #1036: same wording, two causes). Quota rejections always carry an explicit rate cue
|
|
120
|
+
* ("too many requests", "quota exhausted"), so the ABSENCE of those cues plus the
|
|
121
|
+
* absence of a size phrase means payload overflow. Classifying it as 429 makes Codex
|
|
122
|
+
* back off instead of compacting, which burns retries on an unfixable-by-retry failure.
|
|
123
|
+
*/
|
|
124
|
+
const BARE_RE_TAILS = new Set(["error", "", "resource_exhausted", "resource exhausted"]);
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Size prior for bare resource_exhausted classification (devlog 260, live probe 210):
|
|
128
|
+
* a plan-gated model returns the SAME bare RE shape on a ~20-token prompt that a real
|
|
129
|
+
* payload overflow produces, so the message alone cannot separate "compact and retry"
|
|
130
|
+
* from "this account cannot use this model". When the caller can supply how large the
|
|
131
|
+
* request actually was relative to the model's window, a small request keeps the
|
|
132
|
+
* 429-class mapping; only a plausibly-large one classifies as overflow. Unknown
|
|
133
|
+
* sizes keep today's overflow mapping so the prior only ever REMOVES false overflows
|
|
134
|
+
* it can prove.
|
|
135
|
+
*/
|
|
136
|
+
export interface CursorSizeContext {
|
|
137
|
+
estimatedInputTokens?: number;
|
|
138
|
+
contextWindow?: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const OVERFLOW_MIN_FRACTION = 0.5;
|
|
142
|
+
|
|
143
|
+
function bareReLooksLikeOverflow(context?: CursorSizeContext): boolean {
|
|
144
|
+
if (!context) return true;
|
|
145
|
+
const { estimatedInputTokens, contextWindow } = context;
|
|
146
|
+
if (estimatedInputTokens === undefined || contextWindow === undefined || contextWindow <= 0) return true;
|
|
147
|
+
return estimatedInputTokens >= OVERFLOW_MIN_FRACTION * contextWindow;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function isCursorZeroTokenResourceExhausted(lowerMessage: string): boolean {
|
|
151
|
+
if (!lowerMessage.includes("resource_exhausted") && !lowerMessage.includes("resource exhausted")) return false;
|
|
152
|
+
// Any explicit quota/rate cue wins: this is a real 429.
|
|
153
|
+
if (QUOTA_RATE_CUES.some(cue => lowerMessage.includes(cue))) return false;
|
|
154
|
+
// An explicit size phrase also wins (already handled by the existing classifier).
|
|
155
|
+
if (isCursorRequestTooLargeDetail(lowerMessage)) return false;
|
|
156
|
+
// Extract the tail after the resource_exhausted marker. If it names a specific
|
|
157
|
+
// non-quota, non-size cause, this is NOT bare overflow.
|
|
158
|
+
const idx = Math.max(
|
|
159
|
+
lowerMessage.indexOf("resource_exhausted"),
|
|
160
|
+
lowerMessage.indexOf("resource exhausted"),
|
|
161
|
+
);
|
|
162
|
+
const tail = lowerMessage.slice(idx + "resource_exhausted".length).trim().replace(/^[:\s]+/, "").trim();
|
|
163
|
+
if (!BARE_RE_TAILS.has(tail)) return false;
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
115
167
|
const REQUEST_TOO_LARGE_PATTERNS: (string | RegExp)[] = [
|
|
116
168
|
"tool catalog too large",
|
|
117
169
|
"tool registration too large",
|
|
@@ -144,7 +196,7 @@ export function isCursorRequestTooLargeDetail(lowerMessage: string): boolean {
|
|
|
144
196
|
* The returned prefix string is recognized by `src/lib/errors.ts` `classifyError` keywords,
|
|
145
197
|
* so bridge-level error mapping produces the right Codex error type (rate_limit, auth, etc.).
|
|
146
198
|
*/
|
|
147
|
-
export function classifyCursorError(message: string): string {
|
|
199
|
+
export function classifyCursorError(message: string, sizeContext?: CursorSizeContext): string {
|
|
148
200
|
const lower = message.toLowerCase();
|
|
149
201
|
|
|
150
202
|
if (isCursorBenignCancelError(message)) return "Cursor stream suspended";
|
|
@@ -158,9 +210,16 @@ export function classifyCursorError(message: string): string {
|
|
|
158
210
|
// client-fixable 400; everything else surfaces as a 429 so Codex backs off
|
|
159
211
|
// instead of hammering retries (live evidence: 6x 400 retry storm, devlog
|
|
160
212
|
// 260723_cursor_context_continuity/000_plan.md).
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
213
|
+
if (isCursorRequestTooLargeDetail(lower)) return "Cursor resource limit exceeded";
|
|
214
|
+
// A bare resource_exhausted with no quota cue and no size phrase is payload
|
|
215
|
+
// overflow, not rate limiting. Classifying it as 429 makes Codex back off on a
|
|
216
|
+
// failure that only compaction can fix (senpi #1009 / #1036; research unit T01).
|
|
217
|
+
// Refinement (devlog 260): plan-gated models emit the same bare shape on tiny
|
|
218
|
+
// requests — when the caller proves the request was small, keep the 429 class.
|
|
219
|
+
if (isCursorZeroTokenResourceExhausted(lower)) {
|
|
220
|
+
return bareReLooksLikeOverflow(sizeContext) ? "Cursor context limit exceeded" : "Cursor rate limit exceeded";
|
|
221
|
+
}
|
|
222
|
+
return "Cursor rate limit exceeded";
|
|
164
223
|
}
|
|
165
224
|
|
|
166
225
|
if (
|
|
@@ -220,8 +279,8 @@ export function classifyCursorError(message: string): string {
|
|
|
220
279
|
* Produce a user-facing, secret-safe Cursor error message with an actionable category prefix.
|
|
221
280
|
* Mirrors `safeKiroErrorMessage` / `safeKiroHttpErrorMessage` in kiro-errors.ts.
|
|
222
281
|
*/
|
|
223
|
-
export function safeCursorErrorMessage(rawMessage: string): string {
|
|
224
|
-
const prefix = classifyCursorError(rawMessage);
|
|
282
|
+
export function safeCursorErrorMessage(rawMessage: string, sizeContext?: CursorSizeContext): string {
|
|
283
|
+
const prefix = classifyCursorError(rawMessage, sizeContext);
|
|
225
284
|
const detail = sanitize(rawMessage)
|
|
226
285
|
.replace(/resource[_ ]exhausted/gi, "resource limit exceeded")
|
|
227
286
|
.slice(0, 500);
|
|
@@ -103,6 +103,28 @@ export const CURSOR_ROUTER_MODEL_IDS = [
|
|
|
103
103
|
...CURSOR_ROUTING_LEVELS.map(level => `${CURSOR_AUTO_MODEL_ID}-${level}`),
|
|
104
104
|
] as const;
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Cursor models that cannot see images natively. OpenCodex routes them through the vision
|
|
108
|
+
* sidecar (the catalog still advertises image so Codex can attach). Evidence:
|
|
109
|
+
* - Composer family: Cursor staff — text-only; "Model does not support images"
|
|
110
|
+
* - Auto / router modes: Cursor docs omit Images for Auto Cost; staff — pick Claude/GPT for images
|
|
111
|
+
* - glm-5.2: Cursor docs omit Images; Z.ai GLM-5.2 is text-only (vision is GLM-5V)
|
|
112
|
+
* - glm-5.3: same family; seeded as text-only ahead of Cursor's lineup update
|
|
113
|
+
*
|
|
114
|
+
* Composer ids are enumerated explicitly — prefix wildcard matching is deliberately out of
|
|
115
|
+
* scope here; a live-discovered new Composer slug stays native-path until curated. Everyone
|
|
116
|
+
* else in the static seed (Claude, Gemini, GPT, Kimi, Grok) takes SelectedImage. Other
|
|
117
|
+
* live-discovered ids stay unclassified (native path) until curated.
|
|
118
|
+
*/
|
|
119
|
+
export const CURSOR_NO_VISION_MODELS = [
|
|
120
|
+
...CURSOR_ROUTER_MODEL_IDS,
|
|
121
|
+
"composer-1",
|
|
122
|
+
"composer-2.5",
|
|
123
|
+
"composer-2.5-fast",
|
|
124
|
+
"glm-5.2",
|
|
125
|
+
"glm-5.3",
|
|
126
|
+
] as const;
|
|
127
|
+
|
|
106
128
|
/** Wire id Cursor Connect expects for the auto-router (GetUsableModels returns `default`, not `auto`). */
|
|
107
129
|
export const CURSOR_AUTO_WIRE_MODEL_ID = "default";
|
|
108
130
|
|
|
@@ -214,10 +236,15 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM
|
|
|
214
236
|
{ id: "claude-4.6-opus", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
215
237
|
{ id: "claude-4.6-sonnet", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
216
238
|
{ id: "claude-opus-4-7", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
217
|
-
//
|
|
218
|
-
{
|
|
239
|
+
// Opus Fast families: live GetUsableModels (260822) lists ONLY effort-suffixed wire ids
|
|
240
|
+
// ({base-without-fast}-{effort}-fast; the bare id returns not_found), so every entry
|
|
241
|
+
// carries a tier picker. Live-verified: claude-opus-4-8-high-fast completed a turn.
|
|
242
|
+
// Tiers per the 260822 dump (devlog 260822_senpi_cursor_transfer/300).
|
|
243
|
+
{ id: "claude-opus-4-7-fast", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
244
|
+
{ id: "claude-opus-4-8-fast", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
219
245
|
{ id: "claude-opus-4-8", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
220
246
|
{ id: "claude-opus-5", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
247
|
+
{ id: "claude-opus-5-fast", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
221
248
|
{ id: "claude-fable-5", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
222
249
|
|
|
223
250
|
{ id: "composer-1", contextWindow: CONTEXT_200K },
|
|
@@ -24,8 +24,14 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
|
|
|
24
24
|
// against Anthropic's effort ladder docs and Cursor's live model lineup.
|
|
25
25
|
"claude-fable-5": ["low", "medium", "high", "xhigh", "max"],
|
|
26
26
|
"claude-opus-4-7": ["low", "medium", "high", "xhigh", "max"],
|
|
27
|
+
// Opus Fast tiers from the 260822 GetUsableModels dump (devlog .../300): the wire
|
|
28
|
+
// exposes {base-without-fast}-{effort}-fast only; suffix derivation at the bottom of
|
|
29
|
+
// this file produces those ids. opus-5-fast has no xhigh/max (non-thinking) yet.
|
|
30
|
+
"claude-opus-4-7-fast": ["low", "medium", "high", "xhigh", "max"],
|
|
27
31
|
"claude-opus-4-8": ["low", "medium", "high", "xhigh", "max"],
|
|
32
|
+
"claude-opus-4-8-fast": ["low", "medium", "high", "xhigh", "max"],
|
|
28
33
|
"claude-opus-5": ["low", "medium", "high", "xhigh", "max"],
|
|
34
|
+
"claude-opus-5-fast": ["low", "medium", "high"],
|
|
29
35
|
"claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"],
|
|
30
36
|
"glm-5.2": ["high", "max"],
|
|
31
37
|
// 260814 preemptive: glm-5.3 seeded ahead of Cursor's lineup update. Unlike 5.2, Z.AI folds
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import http2 from "node:http2";
|
|
2
|
+
import { registerOptionalShutdownHook } from "../../lib/optional-shutdown-hooks";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_MAX_SESSIONS = 8;
|
|
5
|
+
const SESSION_CLOSE_TIMEOUT_MS = 2_000;
|
|
6
|
+
|
|
7
|
+
interface PoolEntry {
|
|
8
|
+
readonly session: http2.ClientHttp2Session;
|
|
9
|
+
readonly streams: Set<http2.ClientHttp2Stream>;
|
|
10
|
+
usable: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* HTTP/2 connection pool for Cursor Connect DISCOVERY calls (GetUsableModels).
|
|
15
|
+
* Sessions are keyed by origin (scheme+host+port) and reused to avoid fresh
|
|
16
|
+
* TCP+TLS per call. The Run path deliberately dials its own session: Run
|
|
17
|
+
* streams are long-lived bidi whose lifecycle/EOF semantics are owned by
|
|
18
|
+
* live-transport (see devlog 260822_senpi_cursor_transfer/190 — Run-path
|
|
19
|
+
* pooling is a separate, deliberate unit if ever taken).
|
|
20
|
+
*/
|
|
21
|
+
export class CursorH2SessionPool {
|
|
22
|
+
private readonly entries = new Map<string, PoolEntry>();
|
|
23
|
+
private closed = false;
|
|
24
|
+
|
|
25
|
+
constructor(private readonly maxSessions = DEFAULT_MAX_SESSIONS) {}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Lazily registered on first use so a process that never talks to Cursor registers
|
|
29
|
+
* nothing (optional-subsystem doctrine). The seam is synchronous and best-effort;
|
|
30
|
+
* shutdown() is fire-and-forget there because lifecycle's drainAndShutdown runs
|
|
31
|
+
* under its own absolute deadline.
|
|
32
|
+
*/
|
|
33
|
+
private armShutdownHook: (() => void) | undefined = () => {
|
|
34
|
+
this.armShutdownHook = undefined;
|
|
35
|
+
registerOptionalShutdownHook("cursor-h2-pool", () => { void this.shutdown(); });
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
request(
|
|
39
|
+
url: string,
|
|
40
|
+
headers: http2.OutgoingHttpHeaders,
|
|
41
|
+
): http2.ClientHttp2Stream {
|
|
42
|
+
if (this.closed) throw new Error("Cursor H2 session pool is closed");
|
|
43
|
+
this.armShutdownHook?.();
|
|
44
|
+
const origin = new URL(url).origin;
|
|
45
|
+
const entry = this.usableEntry(origin) ?? this.createEntry(origin);
|
|
46
|
+
try {
|
|
47
|
+
const stream = entry.session.request(headers);
|
|
48
|
+
entry.streams.add(stream);
|
|
49
|
+
stream.once("close", () => { entry.streams.delete(stream); });
|
|
50
|
+
return stream;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
this.drain(entry, true);
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async shutdown(): Promise<void> {
|
|
58
|
+
if (this.closed) return;
|
|
59
|
+
this.closed = true;
|
|
60
|
+
const pending: Promise<void>[] = [];
|
|
61
|
+
for (const entry of [...this.entries.values()]) {
|
|
62
|
+
for (const stream of [...entry.streams]) stream.destroy();
|
|
63
|
+
entry.session.close();
|
|
64
|
+
if (entry.session.destroyed) continue;
|
|
65
|
+
pending.push(new Promise<void>(resolve => {
|
|
66
|
+
const timer = setTimeout(resolve, SESSION_CLOSE_TIMEOUT_MS);
|
|
67
|
+
timer.unref?.();
|
|
68
|
+
entry.session.once("close", () => { clearTimeout(timer); resolve(); });
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
this.entries.clear();
|
|
72
|
+
await Promise.all(pending);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get size(): number { return this.entries.size; }
|
|
76
|
+
|
|
77
|
+
private usableEntry(origin: string): PoolEntry | undefined {
|
|
78
|
+
const entry = this.entries.get(origin);
|
|
79
|
+
if (!entry) return undefined;
|
|
80
|
+
if (entry.usable && !entry.session.closed && !entry.session.destroyed) return entry;
|
|
81
|
+
this.drain(entry, false);
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private createEntry(origin: string): PoolEntry {
|
|
86
|
+
const session = http2.connect(origin);
|
|
87
|
+
const entry: PoolEntry = {
|
|
88
|
+
session,
|
|
89
|
+
streams: new Set(),
|
|
90
|
+
usable: true,
|
|
91
|
+
};
|
|
92
|
+
this.entries.set(origin, entry);
|
|
93
|
+
session.once("goaway", () => { this.drain(entry, true); });
|
|
94
|
+
session.on("error", () => { this.drain(entry, false); });
|
|
95
|
+
session.once("close", () => {
|
|
96
|
+
// Identity check: a stale close event from an old session must not evict
|
|
97
|
+
// a healthy replacement entry that was created after drain() removed the old one.
|
|
98
|
+
if (this.entries.get(origin) === entry) this.entries.delete(origin);
|
|
99
|
+
});
|
|
100
|
+
// Enforce bound: evict oldest when over capacity.
|
|
101
|
+
while (this.entries.size > this.maxSessions) {
|
|
102
|
+
const oldest = this.entries.keys().next().value;
|
|
103
|
+
if (!oldest || oldest === origin) break;
|
|
104
|
+
const old = this.entries.get(oldest);
|
|
105
|
+
if (old) this.drain(old, true);
|
|
106
|
+
}
|
|
107
|
+
return entry;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private drain(entry: PoolEntry, closeSession: boolean): void {
|
|
111
|
+
entry.usable = false;
|
|
112
|
+
for (const stream of [...entry.streams]) stream.destroy();
|
|
113
|
+
entry.streams.clear();
|
|
114
|
+
if (closeSession) entry.session.close();
|
|
115
|
+
// Remove from map by finding the matching key.
|
|
116
|
+
for (const [key, value] of this.entries) {
|
|
117
|
+
if (value === entry) { this.entries.delete(key); break; }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Shared singleton pool for all Cursor adapter H2 traffic. */
|
|
123
|
+
export const cursorH2Pool = new CursorH2SessionPool();
|