codex-copilot-dx 0.9.20 → 0.9.21

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
@@ -62,6 +62,9 @@ The dashboard does not poll in the background; refresh when needed.
62
62
  break continuations. Generated images and Codex transcripts are not deleted.
63
63
  - Optional image setup/maintenance never blocks the primary service. Disabled
64
64
  users get no CCDX image skill or MCP installation.
65
+ - CCDX adds experimental local history compression only when installed App
66
+ runtimes support it and report it off. Existing choices are preserved; an
67
+ already-enabled default, unavailable runtime or failed check is skipped.
65
68
  - Image API redirects must stay on the same HTTPS origin. Returned images must
66
69
  use public HTTPS URLs; well-known NAT64 mappings follow IPv4 address rules.
67
70
  - Keep the adapter on loopback. LAN mode is only for trusted networks; do not
package/bin/cli.mjs CHANGED
@@ -24,6 +24,7 @@ import { createProfileModelRuntime } from "../src/profile-model-runtime.mjs";
24
24
  import { createCodexModelCatalog } from "../src/codex-model-catalog.mjs";
25
25
  import { readImageProviderConfig } from "../src/image-provider-config.mjs";
26
26
  import { setResponseHistoryMaxBytes } from "../src/response-history.mjs";
27
+ import { ensureCodexThreadCompression } from "../src/codex-thread-compression.mjs";
27
28
 
28
29
  const LOCAL_VERSION = localPackageVersion();
29
30
  const CLI_NAME = cliCommandName();
@@ -259,7 +260,7 @@ async function reuseRunningAdapterIfAvailable() {
259
260
  if (!running.ok) return false;
260
261
 
261
262
  console.log(status("ok", `Using existing adapter at ${running.baseUrl}`));
262
- await configureCodexClient();
263
+ const { compressionSetup } = await configureCodexClient();
263
264
 
264
265
  await openCodex();
265
266
  console.log(`
@@ -269,6 +270,7 @@ async function reuseRunningAdapterIfAvailable() {
269
270
 
270
271
  Tip: Run ${CLI_NAME} help to view more features and commands.
271
272
  `);
273
+ await compressionSetup;
272
274
  return true;
273
275
  }
274
276
 
@@ -283,6 +285,7 @@ async function configureCodexClient() {
283
285
  console.warn(status("warn", `Image guidance could not be updated: ${error.message}`));
284
286
  }
285
287
  }
288
+ return { compressionSetup: runInBackground(ensureCodexThreadCompression) };
286
289
  }
287
290
 
288
291
  if (CLI.command === "doctor") {
@@ -359,7 +362,7 @@ try {
359
362
  });
360
363
 
361
364
  // Point Codex App at the adapter.
362
- await configureCodexClient();
365
+ const { compressionSetup } = await configureCodexClient();
363
366
 
364
367
  // Launch Codex when available.
365
368
  await openCodex();
@@ -404,6 +407,7 @@ try {
404
407
  };
405
408
  process.once("SIGINT", () => { void shutdown("SIGINT"); });
406
409
  process.once("SIGTERM", () => { void shutdown("SIGTERM"); });
410
+ await compressionSetup;
407
411
  } catch (e) {
408
412
  console.error(status("err", e.message));
409
413
  await closeHttpServer(activeServer, { timeoutMs: RUNTIME.shutdownTimeoutMs }).catch(() => {});
package/docs/README.md CHANGED
@@ -16,6 +16,7 @@ test counts, timings, live-provider observations, and limitations remain
16
16
  historical facts rather than current guarantees.
17
17
 
18
18
  - [0.9.19: GitHub startup checks and stable-release updates](RELEASE_0.9.19.md)
19
+ - [0.9.21: compatibility-gated local history compression](RELEASE_0.9.21.md)
19
20
  - [0.9.20: timing evidence, real-client checks and bounded analytics reuse](RELEASE_0.9.20.md)
20
21
  - [0.9.13: model-bound Auto-review reasoning selection](RELEASE_0.9.13.md)
21
22
  - [0.9.12: GPT-6.1 Sol / low Auto-review default](RELEASE_0.9.12.md)
package/docs/REFERENCE.md CHANGED
@@ -65,6 +65,10 @@ The `OPENAI_API_KEY=dummy` value used in the Codex shell environment is only a c
65
65
 
66
66
  On startup, CCDX also adds `context_management = true` under `[features]` when the key is missing, creating the section if needed. Existing values, including `false`, are preserved. Existing inline or dotted `features` declarations and nested context-management settings are left intact to avoid conflicting TOML definitions. This opts unconfigured users into Codex context management; support and behavior depend on the installed Codex version. A configuration that already matches is not rewritten.
67
67
 
68
+ Startup and adapter reuse also attempt an optional `local_thread_store_compression = true` default under `[features]`. CCDX probes installed macOS App runtimes with `features list`, using an empty temporary Codex directory, no inherited credentials/provider overrides, a 1.5-second timeout per runtime and a 128 KiB output limit. All discovered App readers must advertise a supported, disabled feature; missing/removed/unknown/default-on features or failed probes skip setup. Existing user settings (including `false`, inline/dotted forms and legacy profile overrides) are preserved without probing. Alternative feature-table declarations that cannot be safely extended are left intact. The shared config is reread after probing, and only this key can change. Temporary probe directories are removed; no user transcript is read or compressed by CCDX itself, and no upstream or npm lookup is added. Ready and core requests do not wait for this optional task; its failures never abort the main service. On a first launch, the App may read the config before this background task finishes; the setting is available on its next config load/restart.
69
+
70
+ This remains an experimental Codex storage feature. Current Codex compresses cold local transcript files, including shared histories, with Zstandard; every external reader of that Codex directory must also support the format. It does not reduce model context, request-body limits or CCDX's in-memory history cache, and CCDX does not promise faster inference. Existing third-party transcript readers are not detected. `ccdx doctor config` remains a config-only preview of essential startup settings; it does not spawn runtimes or predict this optional compatibility-gated default.
71
+
68
72
  If a compatible adapter is already running, a later launch reuses it and checks the local Codex configuration. Reuse requires the same package version, protocol version, and required capabilities. After updating the package, stop the old adapter before starting the new version. Installing or updating the package alone does not restart the adapter or update Codex configuration.
69
73
 
70
74
  ### Codex configuration writes
@@ -78,6 +82,7 @@ Normal startup and reuse of an existing adapter check `~/.codex/config.toml` onc
78
82
  | `model_auto_compact_token_limit` | Add `900000` only when missing | `900000` |
79
83
  | `[shell_environment_policy.set]` OpenAI URL/key | Update `OPENAI_BASE_URL` and set `OPENAI_API_KEY="dummy"` when this section is recognized; leave an absent section absent | Create the section and set both keys; set `shell_environment_policy.inherit="core"` |
80
84
  | `[features]` `context_management` | Add `true` when missing; preserve explicit values and alternative declarations as described above | `true` |
85
+ | `[features]` `local_thread_store_compression` | Optional compatibility-gated default; preserve existing values, skip already-on/unsupported/unavailable runtimes | `true` only after a successful probe |
81
86
  | `[mcp_servers.ccdx_image]` | Added in a marked CCDX-owned block only after `ccdx enable-image`; removed by `ccdx disable-image` | Absent by default |
82
87
 
83
88
  The two token limits are fixed defaults, not detected model capabilities. Existing custom values are retained. Writes target the listed keys with format-preserving edits and atomic replacement; this is not a general TOML formatter. Quoted keys, quoted table headers, and explicit dotted shell-environment declarations are recognized; inline shell-environment tables are left intact. Startup and image configuration commands validate UTF-8/TOML and verify that unrelated settings retain their values before writing. An unsafe edit leaves the original file unchanged. Use `ccdx doctor config` to inspect parsed values and preview what this writer would do with the current file. No actual change means no file rewrite.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.9.20",
3
+ "version": "0.9.21",
4
4
  "description": "Use Codex App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
@@ -0,0 +1,90 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { execFile } from "node:child_process";
5
+ import { promisify } from "node:util";
6
+ import { CODEX_APP_BINARY_PATHS } from "./codex-model-catalog.mjs";
7
+ import { parseCodexToml, codexTomlStatements, validateManagedConfigEdit } from "./config-toml.mjs";
8
+ import { atomicWriteFileIfChangedSync } from "./atomic-file.mjs";
9
+ import { status } from "./status.mjs";
10
+
11
+ const FEATURE = "local_thread_store_compression";
12
+ const FEATURE_PATH = ["features", FEATURE];
13
+
14
+ function configured(config) {
15
+ if (Object.hasOwn(config, FEATURE) || Object.hasOwn(config.features || {}, FEATURE)) return true;
16
+ return Object.values(config.profiles || {}).some(profile => Object.hasOwn(profile?.features || {}, FEATURE));
17
+ }
18
+
19
+ export function computeThreadCompressionDefault(content) {
20
+ const config = parseCodexToml(content);
21
+ if (configured(config)) return { content, changed: false };
22
+ const statements = codexTomlStatements(content);
23
+ // Only append to an explicit features table or create an absent one. Preserve
24
+ // inline/dotted/nested declarations rather than risking a conflicting table.
25
+ const table = statements.find(item => item.kind === "table"
26
+ && item.keys.length === 1 && item.keys[0] === "features");
27
+ if (!table && Object.hasOwn(config, "features")) return { content, changed: false };
28
+ const lines = content.split("\n");
29
+ if (table) {
30
+ const end = statements.find(item => item.kind === "table" && item.start > table.start)?.line ?? lines.length;
31
+ lines.splice(end, 0, `${FEATURE} = true`);
32
+ } else {
33
+ while (lines.length && !lines.at(-1).trim()) lines.pop();
34
+ if (lines.length) lines.push("");
35
+ lines.push("[features]", `${FEATURE} = true`, "");
36
+ }
37
+ const joined = lines.join("\n");
38
+ const updated = joined + (content.endsWith("\n") && !joined.endsWith("\n") ? "\n" : "");
39
+ validateManagedConfigEdit(config, updated, [FEATURE_PATH]);
40
+ return { content: updated, changed: true };
41
+ }
42
+
43
+ export async function codexNeedsThreadCompressionDefault({
44
+ binaryPaths = CODEX_APP_BINARY_PATHS,
45
+ execFileFn = promisify(execFile),
46
+ } = {}) {
47
+ const binaries = new Set();
48
+ for (const binary of binaryPaths) {
49
+ try {
50
+ if (path.isAbsolute(binary) && fs.statSync(binary).isFile()) binaries.add(fs.realpathSync(binary));
51
+ } catch {}
52
+ }
53
+ if (!binaries.size) return false;
54
+ const home = await fs.promises.mkdtemp(path.join(os.tmpdir(), "ccdx-compression-probe-"));
55
+ try {
56
+ const defaults = await Promise.all([...binaries].map(async binary => {
57
+ try {
58
+ const { stdout } = await execFileFn(binary, ["features", "list"], {
59
+ cwd: home,
60
+ env: { CODEX_HOME: home, PATH: [path.dirname(process.execPath), "/usr/bin", "/bin", "/usr/sbin", "/sbin"].join(path.delimiter) },
61
+ encoding: "utf8", timeout: 1500, maxBuffer: 128 * 1024, windowsHide: true,
62
+ });
63
+ const rows = String(stdout).split("\n").filter(line => line.trim().split(/\s+/)[0] === FEATURE);
64
+ return rows.length === 1
65
+ && /^local_thread_store_compression\s+(?:under development|experimental|beta|stable)\s+false\s*$/i.test(rows[0].trim());
66
+ } catch { return false; }
67
+ }));
68
+ return defaults.every(Boolean);
69
+ } finally { await fs.promises.rm(home, { recursive: true, force: true }); }
70
+ }
71
+
72
+ export async function ensureCodexThreadCompression({
73
+ filePath = path.join(os.homedir(), ".codex", "config.toml"),
74
+ probe = codexNeedsThreadCompressionDefault,
75
+ } = {}) {
76
+ try {
77
+ const read = () => new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(fs.readFileSync(filePath));
78
+ if (configured(parseCodexToml(read()))) return;
79
+ if (!await probe()) return;
80
+ // The client/user may edit the shared file while the asynchronous probe runs.
81
+ // Recompute against its current bytes; explicit opt-outs always win.
82
+ const updated = computeThreadCompressionDefault(read());
83
+ if (updated.changed) {
84
+ atomicWriteFileIfChangedSync(filePath, updated.content);
85
+ console.log(status("ok", "Enabled Codex local history compression (experimental)"));
86
+ }
87
+ } catch {
88
+ console.warn(status("warn", "Optional Codex history compression setup was skipped; core proxy remains available"));
89
+ }
90
+ }