@bman654/clodex 2.11.6 → 2.12.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/README.md CHANGED
@@ -106,6 +106,9 @@ flowchart LR
106
106
  > [!TIP]
107
107
  > Using Claude Code's agents view or background agents? Ask your Claude Code agent to read [docs/background-agents.md](docs/background-agents.md) and set it up for you — one global `clodex server --proxy` plus the `clodex-claude` wrapper bin bridges every claude process automatically.
108
108
 
109
+ > [!TIP]
110
+ > On Windows with the Claude Code VS Code extension? See [docs/windows-setup.md](docs/windows-setup.md) — proxy-mode env vars route the extension through clodex, and an optional process wrapper makes clodex models appear in its model picker.
111
+
109
112
  ## CLI reference
110
113
 
111
114
  ### `clodex claude [options] [claude-flags]`
package/dist/cli.js CHANGED
@@ -382,7 +382,7 @@ import { join } from "path";
382
382
  // package.json
383
383
  var package_default = {
384
384
  name: "@bman654/clodex",
385
- version: "2.11.6",
385
+ version: "2.12.0",
386
386
  publishConfig: {
387
387
  access: "public"
388
388
  },
@@ -8717,8 +8717,8 @@ var FAILURE_EVENT_TYPES = /* @__PURE__ */ new Set(["error", "response.failed", "
8717
8717
  var RESPONSES_WS_HARD_TTL_MS = 55 * 6e4;
8718
8718
  var RESPONSES_WS_IDLE_TTL_MS = 30 * 6e4;
8719
8719
  var RESPONSES_WS_NURSERY_IDLE_TTL_MS = 5 * 6e4;
8720
- var RESPONSES_WS_MAX_CONNECTIONS = 32;
8721
- var RESPONSES_WS_MAX_NURSERY_CONNECTIONS = 8;
8720
+ var RESPONSES_WS_MAX_CONNECTIONS = 64;
8721
+ var RESPONSES_WS_MAX_NURSERY_CONNECTIONS = 48;
8722
8722
  var diagnosticContext = new AsyncLocalStorage();
8723
8723
  function withResponsesWebSocketDiagnosticContext(context, fn) {
8724
8724
  return diagnosticContext.run(context, fn);
@@ -8847,7 +8847,7 @@ function instructionChangeSummary(previous, current) {
8847
8847
  const firstDiffLine = previous.slice(0, prefix).split("\n").length;
8848
8848
  return `instructions changed: previous_chars=${previous.length} current_chars=${current.length} common_prefix_chars=${prefix} common_suffix_chars=${suffix} first_diff_line=${firstDiffLine}`;
8849
8849
  }
8850
- function responsesWebSocketPartitionKey(wsUrl, payload, options = {}, authorizationFingerprint = "") {
8850
+ function responsesWebSocketPartitionKey(wsUrl, payload, options = {}, authorizationFingerprint = "", claudeAgentId = "") {
8851
8851
  const promptCacheKey = payload.prompt_cache_key;
8852
8852
  const model = payload.model;
8853
8853
  if (typeof promptCacheKey !== "string" || !promptCacheKey || typeof model !== "string" || !model) return void 0;
@@ -8860,7 +8860,8 @@ function responsesWebSocketPartitionKey(wsUrl, payload, options = {}, authorizat
8860
8860
  model,
8861
8861
  effort,
8862
8862
  promptCacheKey,
8863
- authorizationFingerprint
8863
+ authorizationFingerprint,
8864
+ claudeAgentId
8864
8865
  ].join("");
8865
8866
  return createHash6("sha256").update(material).digest("hex");
8866
8867
  }
@@ -9100,13 +9101,16 @@ function mismatchDumpLine(items, index) {
9100
9101
  function canonicalItemStrings(items) {
9101
9102
  return items.map((item) => canonicalJson(normalizeToolCallJson([item])));
9102
9103
  }
9103
- function isStrictPrefix(head, client) {
9104
- if (client.length <= head.length) return false;
9104
+ function isPrefixOrEqual(head, client) {
9105
+ if (client.length < head.length) return false;
9105
9106
  for (let index = 0; index < head.length; index += 1) {
9106
9107
  if (head[index] !== client[index]) return false;
9107
9108
  }
9108
9109
  return true;
9109
9110
  }
9111
+ function isStrictPrefix(head, client) {
9112
+ return client.length > head.length && isPrefixOrEqual(head, client);
9113
+ }
9110
9114
  function continuationMatch(entry, payload, clientItems) {
9111
9115
  if (!entry.responseId || !entry.requestInput || !entry.expectedAssistant) return void 0;
9112
9116
  const full = inputArray(payload);
@@ -9599,11 +9603,16 @@ function evictOldestIdleGeneration(generation, maxConnections, reason) {
9599
9603
  while (connectionCountByGeneration(generation) >= maxConnections && idle.length) {
9600
9604
  const oldest = idle.shift();
9601
9605
  if (oldest) {
9606
+ const idleMs = Math.max(0, oldest.options.now() - oldest.lastUsedAt);
9607
+ oldest.debug(
9608
+ `evicting the oldest idle ${generation} connection to stay within its cap: connection=${oldest.debugId} idle_ms=${idleMs} cap=${maxConnections} reason=${reason}`
9609
+ );
9602
9610
  evictions.push({
9603
9611
  connectionId: oldest.debugId,
9604
9612
  partitionKey: oldest.key,
9605
9613
  generation: oldest.generation,
9606
- reason
9614
+ reason,
9615
+ idleMs
9607
9616
  });
9608
9617
  deleteEntry(oldest);
9609
9618
  }
@@ -10011,28 +10020,42 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
10011
10020
  }
10012
10021
  if (hasResponsesLiteHeader(headers)) payload = applyResponsesLiteShape(payload);
10013
10022
  const authorizationFingerprint = authorizationHeaderFingerprint(headers);
10023
+ const diagnosticCorrelation = diagnosticContext.getStore();
10024
+ const claudeAgentId = diagnosticCorrelation?.claudeAgentId ?? "";
10014
10025
  const partitionKey = responsesWebSocketPartitionKey(
10015
10026
  wsUrl,
10016
10027
  payload,
10017
10028
  options,
10018
- authorizationFingerprint
10029
+ authorizationFingerprint,
10030
+ claudeAgentId
10019
10031
  );
10020
10032
  const promptFingerprint = responsesWebSocketPromptFingerprint(payload);
10021
10033
  const promptFieldHashes = responsesWebSocketPromptFieldHashes(payload);
10022
10034
  const instructionsSnapshot = instructionsFromPayload(payload);
10023
- const diagnosticCorrelation = diagnosticContext.getStore();
10024
10035
  let now = resolvedOptions.now();
10025
10036
  const evictions = cleanupExpiredConnections(now);
10037
+ let canonicalClientItems;
10038
+ const clientItems = () => canonicalClientItems ??= canonicalItemStrings(inputArray(payload));
10026
10039
  const scanForHeads = () => {
10027
10040
  const scanned = partitionKey ? connectionEntries(partitionKey) : [];
10028
10041
  const idle = scanned.filter((entry) => !entry.inFlight);
10029
- const clientItems = idle.length ? canonicalItemStrings(inputArray(payload)) : [];
10042
+ const canonical = idle.length ? clientItems() : [];
10030
10043
  return {
10031
10044
  candidates: scanned,
10032
10045
  idleCandidates: idle,
10033
- matches: idle.map((entry) => ({ entry, match: continuationMatch(entry, payload, clientItems) })).filter((candidate) => candidate.match !== void 0).sort((left, right) => left.match.delta.length - right.match.delta.length || (left.match.mode === right.match.mode ? 0 : left.match.mode === "exact" ? -1 : 1))
10046
+ matches: idle.map((entry) => ({ entry, match: continuationMatch(entry, payload, canonical) })).filter((candidate) => candidate.match !== void 0).sort((left, right) => left.match.delta.length - right.match.delta.length || (left.match.mode === right.match.mode ? 0 : left.match.mode === "exact" ? -1 : 1))
10034
10047
  };
10035
10048
  };
10049
+ const couldPrecedeThisRequest = (entry) => {
10050
+ if (entry.responseId && entry.requestInput && entry.expectedAssistant) {
10051
+ return continuationMatch(entry, payload, clientItems()) !== void 0;
10052
+ }
10053
+ const streaming = entry.current;
10054
+ if (!streaming) return true;
10055
+ streaming.canonicalInput ??= canonicalItemStrings(inputArray(streaming.originalPayload));
10056
+ return isPrefixOrEqual(streaming.canonicalInput, clientItems());
10057
+ };
10058
+ const blockingHead = (entries) => entries.find((entry) => entry.inFlight && couldPrecedeThisRequest(entry));
10036
10059
  let { candidates, idleCandidates, matches } = scanForHeads();
10037
10060
  let selected = matches[0]?.entry;
10038
10061
  let selectedMatch = matches[0]?.match;
@@ -10075,9 +10098,10 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
10075
10098
  );
10076
10099
  return "continuation";
10077
10100
  };
10101
+ let arrivalBlockingHead = selected ? void 0 : blockingHead(candidates);
10078
10102
  if (selected && selectedDelta) {
10079
10103
  decision = continueOnHead(selected, selectedMatch);
10080
- } else if (candidates.some((entry) => entry.inFlight)) {
10104
+ } else if (arrivalBlockingHead) {
10081
10105
  selected = void 0;
10082
10106
  persistent = false;
10083
10107
  decision = "parallel_isolated";
@@ -10176,11 +10200,15 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
10176
10200
  pacingRescanOutcome = "no_change";
10177
10201
  }
10178
10202
  }
10179
- if (!selected && persistent && partitionKey && connectionEntries(partitionKey).some((entry) => entry.inFlight)) {
10180
- persistent = false;
10181
- decision = "parallel_isolated";
10182
- debug("parallel request using an isolated socket after pacing");
10183
- if (pacingRescanOutcome === "no_change") pacingRescanOutcome = "parallel_isolated";
10203
+ if (!selected && persistent && partitionKey) {
10204
+ const blocked = blockingHead(connectionEntries(partitionKey));
10205
+ if (blocked) {
10206
+ arrivalBlockingHead = blocked;
10207
+ persistent = false;
10208
+ decision = "parallel_isolated";
10209
+ debug("parallel request using an isolated socket after pacing");
10210
+ if (pacingRescanOutcome === "no_change") pacingRescanOutcome = "parallel_isolated";
10211
+ }
10184
10212
  }
10185
10213
  }
10186
10214
  let suppressedMismatchWarnings;
@@ -10211,7 +10239,9 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
10211
10239
  accountIdHash: options.accountId ? createHash6("sha256").update(options.accountId).digest("hex").slice(0, 16) : "",
10212
10240
  model: typeof payload.model === "string" ? payload.model : void 0,
10213
10241
  effort: typeof payload.reasoning?.effort === "string" ? String(payload.reasoning.effort).trim().toLowerCase() : "",
10214
- promptCacheKey: typeof payload.prompt_cache_key === "string" ? payload.prompt_cache_key : void 0
10242
+ promptCacheKey: typeof payload.prompt_cache_key === "string" ? payload.prompt_cache_key : void 0,
10243
+ claudeAgentId: claudeAgentId || void 0,
10244
+ claudeParentAgentId: diagnosticCorrelation?.claudeParentAgentId
10215
10245
  },
10216
10246
  promptFingerprint,
10217
10247
  promptFieldHashes,
@@ -10236,6 +10266,10 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
10236
10266
  createdConnectionId: selected ? void 0 : nextConnectionDebugId,
10237
10267
  ...pacingWaitedMs !== void 0 ? { pacingWaitedMs } : {},
10238
10268
  ...pacingRescanOutcome !== void 0 ? { pacingRescanOutcome } : {},
10269
+ // Which busy head this request could not be told apart from. Without it an
10270
+ // isolated turn reads as unexplained, and isolation is the single largest
10271
+ // source of uncached prompt tokens on this transport.
10272
+ ...decision === "parallel_isolated" && arrivalBlockingHead ? { isolatedByConnectionId: arrivalBlockingHead.debugId } : {},
10239
10273
  ...suppressedMismatchWarnings !== void 0 ? { suppressedMismatchWarnings } : {},
10240
10274
  createdGeneration: selected ? void 0 : persistent ? "nursery" : "isolated",
10241
10275
  incrementalInputItems: selectedDelta?.length,
@@ -12105,6 +12139,18 @@ function extractClaudeSessionId(body, headerFallback) {
12105
12139
  }
12106
12140
  return validClaudeSessionId(headerFallback);
12107
12141
  }
12142
+ var CLAUDE_AGENT_ID_RE = /^[A-Za-z0-9._-]{1,128}$/;
12143
+ function extractClaudeAgentIds(headers) {
12144
+ const read = (name) => {
12145
+ const raw = headers[name];
12146
+ const value = (Array.isArray(raw) ? raw[0] : raw)?.trim();
12147
+ return value && CLAUDE_AGENT_ID_RE.test(value) ? value : void 0;
12148
+ };
12149
+ return {
12150
+ claudeAgentId: read("x-claude-code-agent-id"),
12151
+ claudeParentAgentId: read("x-claude-code-parent-agent-id")
12152
+ };
12153
+ }
12108
12154
  function claudeSessionPromptCacheKey(sessionId) {
12109
12155
  return "relay-session-" + createHash8("sha256").update(sessionId).digest("hex").slice(0, 32);
12110
12156
  }
@@ -13316,6 +13362,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
13316
13362
  const openAiOAuth = isOpenAiOAuthRoute(route);
13317
13363
  const claudeSessionIdHeader = Array.isArray(req.headers["x-claude-code-session-id"]) ? req.headers["x-claude-code-session-id"][0] : req.headers["x-claude-code-session-id"];
13318
13364
  const claudeSessionId = extractClaudeSessionId(anthropicBody, claudeSessionIdHeader);
13365
+ const claudeAgentIds = extractClaudeAgentIds(req.headers);
13319
13366
  const translationLifecycle = createTranslationLifecycle(
13320
13367
  inferenceLogPath,
13321
13368
  relayRequestId,
@@ -13389,7 +13436,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
13389
13436
  keepAlive.unref();
13390
13437
  try {
13391
13438
  await withResponsesWebSocketDiagnosticContext(
13392
- { requestId: relayRequestId, claudeSessionId },
13439
+ { requestId: relayRequestId, claudeSessionId, ...claudeAgentIds },
13393
13440
  () => streamAnthropicResponse(
13394
13441
  model,
13395
13442
  params,
@@ -13420,7 +13467,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
13420
13467
  res.end();
13421
13468
  } else {
13422
13469
  const anthropicResponse = await withResponsesWebSocketDiagnosticContext(
13423
- { requestId: relayRequestId, claudeSessionId },
13470
+ { requestId: relayRequestId, claudeSessionId, ...claudeAgentIds },
13424
13471
  () => generateAnthropicResponse(
13425
13472
  model,
13426
13473
  params,
@@ -18189,6 +18236,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
18189
18236
  const requestId = randomUUID5();
18190
18237
  const claudeSessionIdHeader = Array.isArray(req.headers["x-claude-code-session-id"]) ? req.headers["x-claude-code-session-id"][0] : req.headers["x-claude-code-session-id"];
18191
18238
  const claudeSessionId = extractClaudeSessionId(body, claudeSessionIdHeader);
18239
+ const claudeAgentIds = extractClaudeAgentIds(req.headers);
18192
18240
  if (options.webSocketDiagnosticsLogPath) {
18193
18241
  writeWebSocketDiagnosticRequestLog(options.webSocketDiagnosticsLogPath, {
18194
18242
  requestId,
@@ -18353,7 +18401,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
18353
18401
  res.write(chunk);
18354
18402
  };
18355
18403
  await withResponsesWebSocketDiagnosticContext(
18356
- { requestId, claudeSessionId },
18404
+ { requestId, claudeSessionId, ...claudeAgentIds },
18357
18405
  () => streamAnthropicResponse(languageModel, params, responseModelId, writeStreamChunk, void 0, {
18358
18406
  abortSignal: clientAbort.signal,
18359
18407
  initialInputTokens: estimateAnthropicInputTokens(body),
@@ -18369,7 +18417,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
18369
18417
  res.end();
18370
18418
  } else {
18371
18419
  const anthropicResponse = await withResponsesWebSocketDiagnosticContext(
18372
- { requestId, claudeSessionId },
18420
+ { requestId, claudeSessionId, ...claudeAgentIds },
18373
18421
  () => generateAnthropicResponse(languageModel, params, responseModelId, {
18374
18422
  forceStream: openAiOAuth,
18375
18423
  abortSignal: clientAbort.signal,
@@ -19462,6 +19510,9 @@ function forwardToAdapter(req, res, rawBody, adapter, adapterRequest = http2.req
19462
19510
  "Content-Length": String(rawBody.length),
19463
19511
  "x-api-key": adapter.token,
19464
19512
  ...typeof req.headers["x-claude-code-session-id"] === "string" ? { "x-claude-code-session-id": req.headers["x-claude-code-session-id"] } : {},
19513
+ // Subagent identity: the relay partitions ChatGPT WebSocket heads by it.
19514
+ ...typeof req.headers["x-claude-code-agent-id"] === "string" ? { "x-claude-code-agent-id": req.headers["x-claude-code-agent-id"] } : {},
19515
+ ...typeof req.headers["x-claude-code-parent-agent-id"] === "string" ? { "x-claude-code-parent-agent-id": req.headers["x-claude-code-parent-agent-id"] } : {},
19465
19516
  ...lifecycle ? { "x-relay-request-id": lifecycle.requestId } : {}
19466
19517
  }
19467
19518
  }, (upstreamRes) => {