codex-copilot-dx 0.6.2 → 0.6.4

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
@@ -29,11 +29,12 @@ npm install -g codex-copilot-dx@latest
29
29
  ccdx
30
30
  ```
31
31
 
32
- The shorter `ccdx` command is the primary launcher. The existing `codex-copilot-dx`
33
- command remains an equivalent compatibility alias. For a one-off run without a
34
- global install, use `npx codex-copilot-dx@latest`.
35
- Both installed commands expose the same options and subcommands; help, version,
36
- doctor, status, and argument-error output use the command name that was invoked.
32
+ `ccdx` is the only documented command and primary launcher. The old
33
+ `codex-copilot-dx` executable remains temporarily as a compatibility shim: it
34
+ prints a deprecation warning at most once every 7 days in interactive terminals,
35
+ runs `ccdx`, and remains silent in scripts. It will be removed in a future
36
+ breaking release. For a one-off run without a global install, use
37
+ `npm exec --yes --package=codex-copilot-dx@latest -- ccdx`.
37
38
  Run `ccdx <command> --help` (including `auth login claude --help` and
38
39
  `pms status --help`) for command-specific behavior and side-effect details.
39
40
 
@@ -49,8 +50,7 @@ starting the adapter, opening device login, reading the local model cache, or
49
50
  consuming an inference request. It lists model IDs by provider with their
50
51
  advertised API capabilities and reports the live Claude/Anthropic count. A model
51
52
  being advertised does not guarantee that a later inference request will avoid
52
- quota, rate-limit, or policy enforcement. `codex-copilot-dx models` is fully
53
- equivalent.
53
+ quota, rate-limit, or policy enforcement.
54
54
 
55
55
  ### Separate GitHub account for Claude
56
56
 
@@ -193,7 +193,7 @@ It saves the selection in `~/.config/codex-copilot-dx/config.json` (or under
193
193
  `XDG_CONFIG_HOME` when set); choosing `gpt-5.5` clears the override and restores
194
194
  the package default. A running 0.5.1+
195
195
  adapter reads the setting on the next Auto-review request, so it does not need to
196
- be restarted. `codex-copilot-dx auto-review-model` is fully equivalent.
196
+ be restarted.
197
197
 
198
198
  To update the globally installed package, choose a source interactively:
199
199
 
@@ -216,8 +216,7 @@ latest commit from `DaleXiao/codex-copilot-dx` `main` and opts in to Git fetchin
216
216
  for that command, as required by npm 12. It does not change npm's persistent
217
217
  `allow-git` setting. Both paths use npm's global installer without shell
218
218
  interpolation. A currently running adapter keeps its loaded version until it is
219
- stopped and started again. The
220
- `codex-copilot-dx update` compatibility command behaves identically.
219
+ stopped and started again.
221
220
  If a configured npm mirror has not synchronized the current release yet, use
222
221
  the GitHub source to install the current `main` revision.
223
222
 
@@ -278,15 +277,13 @@ For a read-only summary of the running adapter, use:
278
277
  ccdx status
279
278
  ```
280
279
 
281
- The compatibility alias supports the same command as
282
- `codex-copilot-dx status`. For the complete machine-readable payload, query the
283
- loopback endpoint directly:
280
+ For the complete machine-readable payload, query the loopback endpoint directly:
284
281
 
285
282
  ```bash
286
283
  curl -s http://127.0.0.1:2026/_ccdx/status | jq
287
284
  ```
288
285
 
289
- The status endpoint is restricted to the socket's real loopback address, even when LAN binding is explicitly enabled. It reports fixed-size request counters, bounded TTFT/TPOT histograms for streaming routes, admission pressure, memory use, response-history size, image queue/cache state, profile model cache counts, PM relay route totals, and token expiry state. It never retains metric samples or includes prompts, completions, tool arguments, image content, account names, or token values. API responses always include a safe `X-Request-Id` for correlation. If the adapter is not running, the CLI prints the exact start-and-retry command. To include the same ID as `request_id` in related terminal and file log lines, start the adapter with:
286
+ The status endpoint is restricted to the socket's real loopback address, even when LAN binding is explicitly enabled. It reports fixed-size request counters, bounded TTFT/TPOT histograms for streaming routes, admission pressure, memory use, response-history size, image queue/cache and adaptive-history state, profile model cache counts, PM relay route totals, and token expiry state. It never retains metric samples or includes prompts, completions, tool arguments, image content, account names, or token values. API responses always include a safe `X-Request-Id` for correlation. If the adapter is not running, the CLI prints the exact start-and-retry command. To include the same ID as `request_id` in related terminal and file log lines, start the adapter with:
290
287
 
291
288
  ```bash
292
289
  ccdx --show-request-id
@@ -306,7 +303,7 @@ ccdx
306
303
  Claude App support is opt-in so the default Codex Desktop and Claude Code setup stays unchanged:
307
304
 
308
305
  ```bash
309
- npx codex-copilot-dx@latest start --configure-claude-app
306
+ npm exec --yes --package=codex-copilot-dx@latest -- ccdx start --configure-claude-app
310
307
  ```
311
308
 
312
309
  The previous `--configure-claude-desktop` spelling remains a compatibility
@@ -316,7 +313,7 @@ key is restored or the profile is configured in that run.
316
313
  Or set:
317
314
 
318
315
  ```bash
319
- CCDX_CONFIGURE_CLAUDE_DESKTOP=1 npx codex-copilot-dx@latest
316
+ CCDX_CONFIGURE_CLAUDE_DESKTOP=1 npm exec --yes --package=codex-copilot-dx@latest -- ccdx
320
317
  ```
321
318
 
322
319
  This writes a local Claude App 3P gateway profile that points to the adapter root URL, such as `http://127.0.0.1:2026`. The profile uses a generated local bearer key unless `CCDX_CLAUDE_DESKTOP_API_KEY` is set. Later starts restore that key only from the active managed profile when its gateway URL matches the adapter. Once that managed profile is active, successful Copilot model refreshes automatically keep its `inferenceModels` list current without changing the gateway key or other profile settings. Restart Claude App after the initial setup. Later model changes require no profile reconfiguration, although a running Claude App may need to be reopened if it has not reloaded the updated profile yet.
@@ -334,9 +331,10 @@ Environment variables:
334
331
  | `CCDX_ALLOW_LAN` | unset | Set to `1` to allow non-loopback `ADAPTER_HOST` values such as `0.0.0.0`; exposes your Copilot-backed adapter beyond this machine |
335
332
  | `CCDX_MAX_BODY_BYTES` | `67108864` | Maximum compressed/raw request body size |
336
333
  | `CCDX_MAX_DECODED_BODY_BYTES` | `134217728` | Maximum decoded request body size after decompression |
337
- | `CCDX_MAX_INFLIGHT_BODY_BYTES` | `33554432` | Shared byte budget for admitted request bodies; a larger single request runs exclusively |
334
+ | `CCDX_MAX_INFLIGHT_BODY_BYTES` | `33554432` | Per-pool byte budget for raw bodies, decoded bodies, and materialized response history; a larger item runs exclusively in its pool |
338
335
  | `CCDX_MAX_QUEUED_REQUESTS` | `16` | Maximum body requests waiting for the shared byte budget |
339
336
  | `CCDX_REQUEST_QUEUE_TIMEOUT_MS` | `120000` | Maximum wait for request-body admission before returning `503` |
337
+ | `CCDX_REQUEST_BODY_TIMEOUT_MS` | `120000` | Maximum time to receive and decode one request body before returning `408` |
340
338
  | `CCDX_MAX_UPSTREAM_BODY_BYTES` | `31457280` | Strict maximum forwarded Responses body size; larger payloads are adapted locally or rejected with `413` |
341
339
  | `CCDX_MAX_SSE_BUFFER_BYTES` | `8388608` | Maximum buffered bytes for one unterminated upstream SSE line/event |
342
340
  | `CCDX_UPSTREAM_TIMEOUT_MS` | `120000` | Timeout for non-streaming upstream Copilot requests |
@@ -392,7 +390,7 @@ Set `CCDX_LOG_PATH=1` to mirror terminal logs to `~/.local/share/codex-copilot-d
392
390
 
393
391
  ### Image optimization
394
392
 
395
- Long computer-use sessions can accumulate screenshots inside the conversation history. Each screenshot is shipped on later turns, and GitHub Copilot's `/responses` endpoint can reject oversized requests with `408 Request Timeout` while reading the body or `413 Payload Too Large`.
393
+ Long computer-use sessions can accumulate screenshots inside the conversation history. Each screenshot is shipped on later turns, which can increase local preparation time, upstream handshake latency, and the chance of a `413 Payload Too Large` response.
396
394
 
397
395
  The adapter automatically downsamples embedded screenshots to model-appropriate pixel bounds (with a conservative 2048 px long-edge fallback) and initially re-encodes them as WebP quality 82 before forwarding `/v1/responses`. It never replaces an image with a larger encoding and applies one global concurrency limit across direct, function-tool, and custom-tool output images.
398
396
 
@@ -400,6 +398,8 @@ The final serialized UTF-8 request body is measured before forwarding. Above the
400
398
 
401
399
  Completed image transforms are reused from a byte-bounded process-local LRU cache, and concurrent requests for the same transform share one encode. The cache key includes source content, MIME type, model-aware dimensions, WebP quality, and encoder settings; failures are not cached and request cancellation remains isolated.
402
400
 
401
+ Before image encoding, CCDX leaves ordinary visual history byte-for-byte unchanged while it has at most 24 historical images and the expanded body is at most 18 MiB. Above either threshold, the temporary upstream view keeps every current-turn image and at most 16 recent historical images, targeting 16 MiB. If that history tree then times out during local preparation or upstream handshake, the next retry automatically uses an 8-image, 10 MiB recovery window for ten minutes. Two successful requests or a successful compaction clear recovery mode. CCDX never transparently repeats the ambiguous timed-out POST, and no local history is deleted; retrying does not require restarting the service.
402
+
403
403
  When expanded Responses history exceeds Copilot's 50-image request limit, the adapter removes older duplicate image occurrences first, then keeps the 50 most recent images. This applies only to the forwarded request: local history remains complete, current images are preferred over older history, and requests at or below the limit are unchanged.
404
404
 
405
405
  For `/v1/responses/compact`, the adapter sends one terminal `compaction_trigger` in unary mode, validates that Copilot returned completed compaction state, and rebuilds a replayable snapshot from recent system, developer, user, and assistant messages plus every compaction item. Only a validated snapshot becomes a new local history root. Later turns continue from it without re-expanding pre-compaction history; existing older response branches remain available until normal history eviction.
@@ -1,2 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import "./cli.mjs";
2
+
3
+ if (process.stderr.isTTY === true) {
4
+ const {
5
+ LEGACY_COMMAND_WARNING,
6
+ shouldShowLegacyCommandWarning,
7
+ } = await import("../src/legacy-command-warning.mjs");
8
+ if (shouldShowLegacyCommandWarning({ interactive: true })) console.warn(LEGACY_COMMAND_WARNING);
9
+ }
10
+ await import("./cli.mjs");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
@@ -177,6 +177,60 @@ function largeTokenCountProbe(mode) {
177
177
  `);
178
178
  }
179
179
 
180
+ function visualHistoryPressureProbe() {
181
+ return runProbe(`
182
+ import { applyResponsesImagePressure } from "./src/responses-image-pressure.mjs";
183
+
184
+ const concurrency = 4;
185
+ const historicalImages = 36;
186
+ const imageBytes = 256 * 1024;
187
+ const dataUrls = Array.from({ length: historicalImages }, (_, index) => (
188
+ \`data:image/png;base64,\${Buffer.alloc(imageBytes, index + 1).toString("base64")}\`
189
+ ));
190
+ const contexts = Array.from({ length: concurrency }, (_, requestIndex) => ({
191
+ historyRootId: \`resp_benchmark_\${requestIndex}\`,
192
+ currentInputStart: historicalImages,
193
+ body: {
194
+ model: "gpt-5.6-sol",
195
+ stream: true,
196
+ input: [
197
+ ...dataUrls.map((imageUrl) => ({
198
+ type: "message",
199
+ role: "user",
200
+ content: [{ type: "input_image", image_url: imageUrl }],
201
+ })),
202
+ { type: "message", role: "user", content: "continue" },
203
+ ],
204
+ },
205
+ }));
206
+ globalThis.gc?.();
207
+ const before = process.memoryUsage();
208
+ let peakRss = before.rss;
209
+ let peakHeap = before.heapUsed;
210
+ const started = performance.now();
211
+ const results = await Promise.all(contexts.map(async (context) => {
212
+ await Promise.resolve();
213
+ const result = applyResponsesImagePressure(context);
214
+ const memory = process.memoryUsage();
215
+ peakRss = Math.max(peakRss, memory.rss);
216
+ peakHeap = Math.max(peakHeap, memory.heapUsed);
217
+ return result;
218
+ }));
219
+ const mib = (bytes) => +(bytes / 1048576).toFixed(1);
220
+ process.stdout.write(JSON.stringify({
221
+ concurrency,
222
+ historical_images_per_request: historicalImages,
223
+ elapsed_ms: +(performance.now() - started).toFixed(1),
224
+ omitted_images: results.map((result) => result.imagesOmitted),
225
+ retained_historical_images: results.map((result) => result.historicalImages),
226
+ input_body_mib: results.map((result) => mib(result.initialBodyBytes)),
227
+ output_body_mib: results.map((result) => mib(result.bodyBytes)),
228
+ rss_peak_delta_mib: mib(peakRss - before.rss),
229
+ heap_peak_delta_mib: mib(peakHeap - before.heapUsed),
230
+ }));
231
+ `);
232
+ }
233
+
180
234
  const adapterImport = runProbe(`
181
235
  const started = performance.now();
182
236
  await import("./src/adapter.mjs");
@@ -324,6 +378,7 @@ if (checkMode) {
324
378
  largePayloadProbe(30, 1),
325
379
  largePayloadProbe(30, 2),
326
380
  ];
381
+ report.visual_history_pressure_36x4 = visualHistoryPressureProbe();
327
382
  } else if (process.argv.includes("--large-payload")) {
328
383
  report.large_payload_peak = [
329
384
  largePayloadProbe(5, 1),
@@ -359,6 +414,12 @@ if (checkMode) {
359
414
  failures.push(`request admission exceeded its budget at ${payload.target_request_mib}MiB x${payload.concurrency}`);
360
415
  }
361
416
  }
417
+ const visualHistory = report.visual_history_pressure_36x4;
418
+ if (!visualHistory.omitted_images.every((count) => count === 20)
419
+ || !visualHistory.retained_historical_images.every((count) => count === 16)
420
+ || !visualHistory.output_body_mib.every((size, index) => size < visualHistory.input_body_mib[index])) {
421
+ failures.push("36-image x4 visual history pressure did not preserve the expected 16-image upstream window");
422
+ }
362
423
  if (failures.length) {
363
424
  for (const failure of failures) console.error(`[FAIL] ${failure}`);
364
425
  process.exitCode = 1;
package/src/adapter.mjs CHANGED
@@ -17,6 +17,7 @@ import { status } from "./status.mjs";
17
17
  import { createTerminalActivityIndicator } from "./terminal-activity.mjs";
18
18
  import { ADAPTER_HEALTH_PATH, adapterHealthPayload } from "./running-adapter.mjs";
19
19
  import { createResponsesCompactHandler, createResponsesHandler } from "./responses-handler.mjs";
20
+ import { createResponsesImagePressureController } from "./responses-image-pressure.mjs";
20
21
  import { loadRuntimeConfig, parsePositiveInteger } from "./runtime-config.mjs";
21
22
  import {
22
23
  createRequestAdmission,
@@ -135,9 +136,11 @@ export function createAdapterHandler(options = {}) {
135
136
  const upstreamTimeoutMs = parsePositiveInteger(options.upstreamTimeoutMs, ADAPTER_RUNTIME_CONFIG.upstreamTimeoutMs);
136
137
  const streamHandshakeTimeoutMs = parsePositiveInteger(options.streamHandshakeTimeoutMs, ADAPTER_RUNTIME_CONFIG.streamHandshakeTimeoutMs);
137
138
  const streamIdleTimeoutMs = parsePositiveInteger(options.streamIdleTimeoutMs, ADAPTER_RUNTIME_CONFIG.streamIdleTimeoutMs);
139
+ const requestBodyTimeoutMs = parsePositiveInteger(options.requestBodyTimeoutMs, ADAPTER_RUNTIME_CONFIG.requestBodyTimeoutMs);
138
140
  const acquireRequest = options.acquireRequest || createRequestAdmission();
139
141
  const requestMetrics = options.requestMetrics || createRequestMetrics();
140
142
  const streamPerformanceMetrics = options.streamPerformanceMetrics || createStreamPerformanceMetrics();
143
+ const imagePressure = options.imagePressure || createResponsesImagePressureController();
141
144
  const claudeDesktopModelOptions = () => {
142
145
  const modelDefs = claudeModelRegistry?.modelDefs || options.claudeDesktopModelDefs;
143
146
  return Array.isArray(modelDefs) ? { modelDefs } : {};
@@ -147,9 +150,12 @@ export function createAdapterHandler(options = {}) {
147
150
  autoReviewModelResolver: options.autoReviewModelResolver,
148
151
  chatCompletionsFn: codexChatCompletionsFn,
149
152
  getCachedModelEndpointsFn,
153
+ imagePressure,
154
+ now: options.now,
150
155
  openAIModelEnv,
151
156
  responsesPayloadOptions: options.responsesPayloadOptions,
152
157
  responsesFn,
158
+ requestBodyTimeoutMs,
153
159
  streamHandshakeTimeoutMs,
154
160
  streamIdleTimeoutMs,
155
161
  upstreamTimeoutMs,
@@ -157,18 +163,22 @@ export function createAdapterHandler(options = {}) {
157
163
  const responsesCompactHandler = createResponsesCompactHandler({
158
164
  acquireRequest,
159
165
  autoReviewModelResolver: options.autoReviewModelResolver,
166
+ imagePressure,
167
+ now: options.now,
160
168
  openAIModelEnv,
169
+ requestBodyTimeoutMs,
161
170
  responsesCompactFn,
162
171
  streamHandshakeTimeoutMs,
163
172
  streamIdleTimeoutMs,
164
173
  upstreamTimeoutMs,
165
174
  });
166
- const anthropicCountTokensHandler = createAnthropicCountTokensHandler({ acquireRequest });
175
+ const anthropicCountTokensHandler = createAnthropicCountTokensHandler({ acquireRequest, requestBodyTimeoutMs });
167
176
  const anthropicMessagesHandler = createAnthropicMessagesHandler({
168
177
  acquireRequest,
169
178
  chatCompletionsFn: claudeChatCompletionsFn,
170
179
  environment: process.env,
171
180
  modelOptions: claudeDesktopModelOptions,
181
+ requestBodyTimeoutMs,
172
182
  streamHandshakeTimeoutMs,
173
183
  streamIdleTimeoutMs,
174
184
  upstreamTimeoutMs,
@@ -180,6 +190,7 @@ export function createAdapterHandler(options = {}) {
180
190
  claudeProfile: options.claudeProfile,
181
191
  claudeModelRegistry,
182
192
  fetchImpl: options.pmStudioFetchImpl,
193
+ requestBodyTimeoutMs,
183
194
  streamHandshakeTimeoutMs,
184
195
  streamIdleTimeoutMs,
185
196
  upstreamTimeoutMs,
@@ -197,6 +208,7 @@ export function createAdapterHandler(options = {}) {
197
208
  metrics: requestMetrics,
198
209
  streamPerformance: streamPerformanceMetrics,
199
210
  admission: acquireRequest,
211
+ imagePressure,
200
212
  modelRegistry: codexModelRegistry,
201
213
  codexClient,
202
214
  claudeClient,
@@ -279,13 +291,20 @@ export function createAdapterHandler(options = {}) {
279
291
  };
280
292
 
281
293
  return (req, res) => {
282
- const pathname = requestPath(req.url);
283
294
  const requestId = createRequestId();
284
295
  if (typeof res.setHeader === "function") {
285
296
  res.setHeader("X-Request-Id", requestId);
286
297
  } else if (res.headers && typeof res.headers === "object") {
287
298
  res.headers["X-Request-Id"] = requestId;
288
299
  }
300
+ let pathname;
301
+ try {
302
+ pathname = requestPath(req.url);
303
+ } catch {
304
+ res.writeHead(400, { "Content-Type": "application/json", Connection: "close" });
305
+ res.end(JSON.stringify({ error: "Invalid request target" }));
306
+ return;
307
+ }
289
308
 
290
309
  const trackRequest = pathname !== ADAPTER_HEALTH_PATH && pathname !== ADAPTER_STATUS_PATH;
291
310
  const routeName = classifyAdapterRoute(req.method, pathname);
@@ -21,20 +21,22 @@ import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
21
21
  import { recordAnthropicUsage } from "./usage.mjs";
22
22
 
23
23
  export function createAnthropicCountTokensHandler(options) {
24
- const { acquireRequest } = options;
24
+ const { acquireRequest, requestBodyTimeoutMs } = options;
25
25
 
26
26
  return async function handleAnthropicCountTokens(req, res) {
27
27
  const abort = createRequestAbort(req, res);
28
28
  let releaseRequest = () => {};
29
29
  try {
30
30
  releaseRequest = await acquireRequest(req, { signal: abort.signal });
31
- const parsed = await readJsonBody(req);
31
+ abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
32
+ const parsed = await readJsonBody(req, { admission: releaseRequest, signal: abort.signal });
33
+ abort.clearTimeout();
32
34
  const result = await countTokens(parsed);
33
35
  releaseRequest();
34
36
  res.writeHead(200, { "Content-Type": "application/json" });
35
37
  res.end(JSON.stringify(result));
36
38
  } catch (error) {
37
- sendJsonError(res, error);
39
+ sendJsonError(res, error, abort.reason === "request_body_timeout" ? 408 : 400);
38
40
  } finally {
39
41
  releaseRequest();
40
42
  abort.cleanup();
@@ -48,6 +50,7 @@ export function createAnthropicMessagesHandler(options) {
48
50
  chatCompletionsFn,
49
51
  environment,
50
52
  modelOptions,
53
+ requestBodyTimeoutMs,
51
54
  streamHandshakeTimeoutMs,
52
55
  streamIdleTimeoutMs,
53
56
  upstreamTimeoutMs,
@@ -58,7 +61,8 @@ export function createAnthropicMessagesHandler(options) {
58
61
  let releaseRequest = () => {};
59
62
  try {
60
63
  releaseRequest = await acquireRequest(req, { signal: abort.signal });
61
- const parsed = await readJsonBody(req);
64
+ abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
65
+ const parsed = await readJsonBody(req, { admission: releaseRequest, signal: abort.signal });
62
66
  abort.setTimeout(
63
67
  parsed.stream ? streamHandshakeTimeoutMs : upstreamTimeoutMs,
64
68
  parsed.stream ? "stream_handshake_timeout" : "upstream_timeout",
@@ -5,7 +5,6 @@ const START_OPTIONS = new Map([
5
5
  ["--configure-claude-app", "configure-claude-app"],
6
6
  ["--show-request-id", "show-request-id"],
7
7
  ]);
8
- const CLI_COMMANDS = new Set(["ccdx", "codex-copilot-dx"]);
9
8
  const PM_COMMANDS = new Set(["pms", "pm-studio"]);
10
9
  const HELP_TOPICS = new Set([
11
10
  "",
@@ -253,10 +252,8 @@ export function parseAdapterProbeOptions(env = process.env) {
253
252
  };
254
253
  }
255
254
 
256
- export function cliCommandName(executable = process.argv[1]) {
257
- const name = String(executable || "").split(/[\\/]/).pop();
258
- if (name === "codex-copilot-dx.mjs") return "codex-copilot-dx";
259
- return CLI_COMMANDS.has(name) ? name : "ccdx";
255
+ export function cliCommandName() {
256
+ return "ccdx";
260
257
  }
261
258
 
262
259
  function topicHelp(name, topic) {
@@ -280,10 +277,9 @@ function topicHelp(name, topic) {
280
277
  }
281
278
 
282
279
  export function cliHelp(commandName = "ccdx", topic = "") {
283
- const name = CLI_COMMANDS.has(commandName) ? commandName : "ccdx";
284
- const alias = name === "ccdx" ? "codex-copilot-dx" : "ccdx";
280
+ const name = "ccdx";
285
281
  const normalizedTopic = checkedHelpTopic(topic ? topic.split(" ") : []);
286
- if (normalizedTopic) return `${topicHelp(name, normalizedTopic)}\n\nEquivalent command: ${alias}`;
282
+ if (normalizedTopic) return topicHelp(name, normalizedTopic);
287
283
  return `Usage:
288
284
  ${name} [start] [--configure-claude-app] [--show-request-id]
289
285
  ${name} auth status [--online]
@@ -310,7 +306,5 @@ Commands:
310
306
  auto-review-model Select the Codex Auto-review Responses model
311
307
  update Update the global package from npm or GitHub
312
308
 
313
- Run ${name} <command> --help for command details.
314
-
315
- Equivalent command: ${alias}`;
309
+ Run ${name} <command> --help for command details.`;
316
310
  }
@@ -149,6 +149,7 @@ export function formatAdapterStatus({ baseUrl, data }, { commandName = "ccdx", c
149
149
  const admission = data.admission || {};
150
150
  const history = data.response_history || {};
151
151
  const images = data.image_optimization || {};
152
+ const imageHistory = data.image_history_pressure;
152
153
  const models = data.models || {};
153
154
  const copilot = data.copilot || {};
154
155
  const limits = data.limits || {};
@@ -166,8 +167,11 @@ export function formatAdapterStatus({ baseUrl, data }, { commandName = "ccdx", c
166
167
  status("info", `Memory: RSS ${mebibytes(processStats.rss_bytes)}, heap ${mebibytes(processStats.heap_used_bytes)}`),
167
168
  status("info", `History: ${mebibytes(history.bytes)} / ${mebibytes(limits.response_history_max_bytes)}, ${count(history.entries)} entries, ${count(history.evicted)} evicted`),
168
169
  status("info", `Image cache: ${count(images.cache_entries)} entries, ${cacheHitRate(images)} hits, ${mebibytes(images.cache_bytes)} / ${mebibytes(images.cache_max_bytes)}`),
169
- status("info", `Models: ${count(models.models)} total, ${count(models.claude_models)} Claude; Copilot token ${copilot.token_cached ? `cached (${duration(copilot.token_expires_in_ms)} remaining)` : "not cached"}`),
170
170
  ];
171
+ if (imageHistory && typeof imageHistory === "object") {
172
+ lines.push(status("info", `Visual history: ${count(imageHistory.active_recovery_trees)} recovery trees, ${count(imageHistory.adapted_requests)} adapted requests, ${count(imageHistory.historical_images_omitted)} older images omitted, ${count(imageHistory.timeouts_recorded)} timeouts`));
173
+ }
174
+ lines.push(status("info", `Models: ${count(models.models)} total, ${count(models.claude_models)} Claude; Copilot token ${copilot.token_cached ? `cached (${duration(copilot.token_expires_in_ms)} remaining)` : "not cached"}`));
171
175
  if (data.profiles?.codex || data.profiles?.claude) {
172
176
  lines.push(status("info", `Profiles: ${profileSummary("Codex", data.profiles?.codex)}; ${profileSummary("Claude", data.profiles?.claude)}`));
173
177
  }
@@ -85,6 +85,7 @@ export function createCopilotClientRuntime({
85
85
  fetchImpl,
86
86
  retryOptions,
87
87
  bodyText,
88
+ onUpstreamStart,
88
89
  } = {}) {
89
90
  const token = await tokenSession.getToken({ signal });
90
91
  const upstreamReq = chatReq.stream === true ? withChatStreamUsage(chatReq) : chatReq;
@@ -99,6 +100,7 @@ export function createCopilotClientRuntime({
99
100
  vision: computeVision(messages),
100
101
  });
101
102
  headers["Content-Length"] = String(Buffer.byteLength(serializedBody));
103
+ onUpstreamStart?.();
102
104
  if (upstreamReq.stream === true) markUpstreamStarted();
103
105
  return fetchCopilotUpstream(`${tokenSession.getApiBase()}/chat/completions`, {
104
106
  method: "POST",
@@ -152,6 +154,7 @@ export function createCopilotClientRuntime({
152
154
  fetchImpl,
153
155
  retryOptions,
154
156
  currentInputStart = 0,
157
+ onUpstreamStart,
155
158
  payloadPrepared = false,
156
159
  payloadOptions = {},
157
160
  } = {}) {
@@ -175,6 +178,7 @@ export function createCopilotClientRuntime({
175
178
  headers["Content-Length"] = String(bodyBytes);
176
179
  headers.Accept = reqBody.stream ? "text/event-stream" : "application/json";
177
180
  try {
181
+ onUpstreamStart?.();
178
182
  if (reqBody.stream === true) markUpstreamStarted();
179
183
  return await fetchCopilotUpstream(`${tokenSession.getApiBase()}${responsesEndpointPath()}`, {
180
184
  method: "POST",
@@ -183,6 +187,7 @@ export function createCopilotClientRuntime({
183
187
  signal,
184
188
  }, { fetchImpl, ...retryOptions });
185
189
  } catch (error) {
190
+ if (signal?.aborted) throw abortError(signal);
186
191
  const cause = error?.cause;
187
192
  const causeText = cause ? ` (${[cause.code, cause.message].filter(Boolean).join(": ")})` : "";
188
193
  throw new Error(`Copilot responses fetch failed: ${error.message}${causeText}`);