From 7269be2e876aeeb439474bf2675544e3c5745e53 Mon Sep 17 00:00:00 2001 From: R <53855466+cb8010d6@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:34:20 +0800 Subject: [PATCH 1/8] feat(codex): opt into client-side compaction --- .../content/docs/guides/codex-integration.md | 40 +++++++++++++++++ .../docs/reference/configuration/server.md | 1 + gui/src/i18n/de.ts | 2 + gui/src/i18n/en.ts | 2 + gui/src/i18n/fr.ts | 2 + gui/src/i18n/ja.ts | 2 + gui/src/i18n/ko.ts | 2 + gui/src/i18n/ru.ts | 2 + gui/src/i18n/tr.ts | 2 + gui/src/i18n/zh-TW.ts | 2 + gui/src/i18n/zh.ts | 2 + gui/src/pages/dashboard-overview-sections.tsx | 20 +++++++++ gui/src/pages/dashboard-shared.ts | 1 + gui/src/pages/use-dashboard-data.ts | 12 ++++-- gui/tests/vision-sidecar-dashboard.test.tsx | 15 +++++++ src/cli/system-command.ts | 7 ++- src/codex/inject.ts | 22 +++++++--- src/config.ts | 1 + src/server/management/config-routes.ts | 29 ++++++++++--- src/types/config.ts | 6 +++ tests/cli/cli-headless-parity.test.ts | 18 ++++++++ .../codex-inject-integration.test.ts | 20 +++++++++ tests/codex-integration/codex-inject.test.ts | 43 +++++++++++++++++++ tests/config/settings-stream-mode.test.ts | 36 ++++++++++++++++ 24 files changed, 273 insertions(+), 16 deletions(-) diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index 5b75a21b41..ee5ffb4da3 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -245,6 +245,46 @@ The cache remains bounded; this does not extend retention or recover history the longer has. HTTP clients must handle the error explicitly and resend their full context without `previous_response_id`. Retrying only the same ID cannot recover missing state. +### Client-side compaction (opt-in) + +Authenticated loopback routing normally keeps Codex on its built-in `openai` provider identity. +That preserves native thread identity, but it also makes Codex request native remote compaction. +When a routed provider cannot return a native compaction blob, OpenCodeX stores the summary in its +own `ocx1:` envelope. Native ChatGPT cannot verify that envelope if OpenCodeX is later removed from +the request path. + +Enable client-side compaction to keep V2 sub-agent routing while preventing new `ocx1:` history: + +```bash +ocx system settings --client-compaction on # or "codexClientCompaction": true in config.json +ocx sync # rewrites ~/.codex/config.toml; restart Desktop +``` + +The setting defaults to off. When enabled, OpenCodeX selects its existing dedicated provider form +with `requires_openai_auth = true`: + +```toml +model_provider = "opencodex" + +[model_providers.opencodex] +name = "OpenCodex Proxy" +base_url = "http://127.0.0.1:10100/v1" +wire_api = "responses" +requires_openai_auth = true +``` + +Codex then owns compaction and stores a portable plaintext summary rather than a new OpenCodeX +envelope. The compacting request still routes through OpenCodeX and can consume quota on the +selected provider. Provider-level V2 policy is independent: plaintext delivery, encrypted task +passthrough through `allowEncryptedV2AgentTasks`, and configured recovery or fallback behavior do +not change. + +This preference affects future compactions only. It does not rewrite existing `ocx1:` history; +use the explicit history recovery workflow for an affected thread. New threads use the +`opencodex` provider identity while the mode is active, so the existing resume-history compatibility +and restore behavior applies. Turning the setting off and syncing restores the default Design B +root override. + ### Authless Codex Desktop (opt-in) In **Dashboard → Overview**, **Open Codex without signing in** controls this existing diff --git a/docs-site/src/content/docs/reference/configuration/server.md b/docs-site/src/content/docs/reference/configuration/server.md index cdc8d6af52..79dc6d1299 100644 --- a/docs-site/src/content/docs/reference/configuration/server.md +++ b/docs-site/src/content/docs/reference/configuration/server.md @@ -27,6 +27,7 @@ runs helper features around provider requests. | `codexAutoStart?` | `boolean` | `true` | Let the Codex shim run `ocx ensure` before launching Codex. False makes ensure a no-op. | | `codexShimAutoRestore?` | `boolean` | `true` | Restore an installed shim after a completed external Codex update replaces it. Environment opt-out: `OPENCODEX_CODEX_SHIM_AUTO_RESTORE=0`. | | `codexDesktopAuthless?` | `boolean` | `false` | Opt-in authless Codex Desktop routing on a loopback bind: inject the dedicated `opencodex` provider with `requires_openai_auth = false` so Desktop opens without a ChatGPT login. Ignored on non-loopback binds. `ocx system settings --desktop-authless on`. See [Codex integration](/guides/codex-integration/#authless-codex-desktop-opt-in). | +| `codexClientCompaction?` | `boolean` | `false` | Opt into Codex client-side compaction on an authenticated loopback bind. Uses the dedicated `opencodex` provider identity with `requires_openai_auth = true`, preventing new routed compactions from storing OpenCodeX-owned `ocx1:` state. V2 sub-agent routing is unchanged. `ocx system settings --client-compaction on`. See [Codex integration](/guides/codex-integration/#client-side-compaction-opt-in). | | `resetCreditAutoRedeem?` | `{ enabled?: boolean; leadTimeMinutes?: number }` | off | Opt-in: redeem the main Codex account's soonest-expiring reset credit `leadTimeMinutes` (1–60, default 10) before it expires. Every attempt re-reads the upstream credit list first and skips when the credit is gone (for example, redeemed by hand); the `redeem_request_id` is journaled in `$OPENCODEX_HOME/reset-credit-auto-redeem.json` before the call so a crash replays the same idempotent request instead of spending a second credit. Servers sharing this configuration directory coordinate reservations and settlements so one process does not replace another's request record. Logs carry a hashed account key only. | | `syncResumeHistory?` | `boolean` | `true` | Reversible Codex App history compatibility. Original metadata is backed up and restored by `ocx stop` / `ocx restore`. | | `shadowCallIntercept?` | `{ enabled?: boolean; model?: string; sourceModels?: string[] }` | off | Redirect recognized Codex helper/shadow calls to a chosen model while preserving the request's configured reasoning effort. The default source prefix is `gpt-5.6-luna`; older clients through 0.144.x used `gpt-5.4-mini`, which `sourceModels` can restore. | diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 2a889b8f2b..86a299bd8c 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -2486,6 +2486,8 @@ export const de: Record = { "dash.visionAdvancedPopover": "Erweiterte Vision-Einstellungen", "dash.codexDesktopAuthless": "Codex ohne Anmeldung öffnen", "dash.codexDesktopAuthlessHint": "Standardmäßig aus. Überspringt die separate Desktop-Anmeldung bei geeigneten lokalen Verbindungen. Zugangsdaten für den Anbieter bleiben erforderlich. Codex nach einer Änderung neu starten. Kontogebundene Desktop-Funktionen können fehlen.", + "dash.codexClientCompaction": "Clientseitige Komprimierung verwenden", + "dash.codexClientCompactionHint": "Standardmäßig aus. Speichert portable Klartext-Zusammenfassungen statt neuer OpenCodeX-ocx1-Daten. Das V2-Subagent-Routing bleibt unverändert. Vorhandene ocx1-Verläufe müssen weiterhin wiederhergestellt werden. Codex nach einer Änderung neu starten.", "models.newPolicyGlobal": "Neue Modelle zunächst deaktivieren", "models.newPolicyProvider": "Richtlinie für neue Modelle", "models.newPolicy_inherit": "Übernehmen", "models.newPolicy_off": "Aus", "models.newPolicy_on": "An", "models.newBadge": "NEU", "models.newCount": "{count} neu, aus", "models.aliases": "Aliase", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 0cf7469f56..af0c2dbf7f 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -579,6 +579,8 @@ export const en = { "dash.multiAgent": "Sub-agent", "dash.codexDesktopAuthless": "Open Codex without signing in", "dash.codexDesktopAuthlessHint": "Off by default. Skip the separate Desktop sign-in for eligible local connections. Upstream credentials are still required. Restart Codex after changing this setting. Account-gated Desktop features may be unavailable.", + "dash.codexClientCompaction": "Use client-side compaction", + "dash.codexClientCompactionHint": "Off by default. Store portable plaintext summaries instead of new OpenCodeX ocx1 state. V2 sub-agent routing is unchanged. Existing ocx1 history still needs recovery. Restart Codex after changing this setting.", "models.v2Conflict": "[agents] max_threads is set — codex will refuse to start; remove it from config.toml", "models.v2Applied": "Sub-agent mode updated — applies to new sessions (restart the Codex app to refresh the picker)", "models.v2ThreadsLabel": "Max threads", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 576c3b7f23..7e85aca05b 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -564,6 +564,8 @@ export const fr: Record = { "dash.multiAgent": "Sous-agent", "dash.codexDesktopAuthless": "Ouvrir Codex sans se connecter", "dash.codexDesktopAuthlessHint": "Désactivé par défaut. Ignore la connexion Desktop séparée pour les connexions locales admissibles. Les identifiants du fournisseur restent nécessaires. Redémarrez Codex après toute modification. Certaines fonctions Desktop liées au compte peuvent être indisponibles.", + "dash.codexClientCompaction": "Utiliser la compaction côté client", + "dash.codexClientCompactionHint": "Désactivé par défaut. Enregistre des résumés portables en texte clair au lieu de nouvelles données OpenCodeX ocx1. Le routage des sous-agents V2 ne change pas. L'historique ocx1 existant doit toujours être restauré. Redémarrez Codex après modification.", "models.v2Conflict": "[agents] max_threads est défini — codex refusera de démarrer ; supprimez-le de config.toml", "models.v2Applied": "Mode sous-agent mis à jour — s’applique aux nouvelles sessions (redémarrez l’application Codex pour actualiser le sélecteur)", "models.v2ThreadsLabel": "Nombre maximal de fils", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 1e01aea545..cb0aa08a52 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -2507,6 +2507,8 @@ export const ja: Record = { "dash.visionAdvancedPopover": "詳細なビジョン設定", "dash.codexDesktopAuthless": "ログインせずに Codex を開く", "dash.codexDesktopAuthlessHint": "既定ではオフです。対象のローカル接続で Desktop の個別ログインを省略します。上流プロバイダーの認証情報は引き続き必要です。変更後は Codex を再起動してください。アカウントに依存する Desktop 機能が利用できない場合があります。", + "dash.codexClientCompaction": "クライアント側コンパクションを使用", + "dash.codexClientCompactionHint": "既定ではオフです。新しい OpenCodeX ocx1 状態ではなく、移植可能な平文要約を保存します。V2 サブエージェントのルーティングは変わりません。既存の ocx1 履歴は別途復旧が必要です。変更後は Codex を再起動してください。", "models.newPolicyGlobal": "新しいモデルを無効で追加", "models.newPolicyProvider": "新しいモデルのポリシー", "models.newPolicy_inherit": "継承", "models.newPolicy_off": "オフ", "models.newPolicy_on": "オン", "models.newBadge": "新着", "models.newCount": "新着 {count} 件、オフ", "models.aliases": "エイリアス", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index f1d20bf65e..58830a885a 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -2508,6 +2508,8 @@ export const ko: Record = { "dash.visionAdvancedPopover": "고급 비전 설정", "dash.codexDesktopAuthless": "로그인 없이 Codex 열기", "dash.codexDesktopAuthlessHint": "기본값은 꺼짐입니다. 지원되는 로컬 연결에서 별도의 Desktop 로그인을 건너뜁니다. 업스트림 인증 정보는 여전히 필요합니다. 변경 후 Codex를 다시 시작하세요. 계정에 연결된 Desktop 기능을 사용하지 못할 수 있습니다.", + "dash.codexClientCompaction": "클라이언트 측 컴팩션 사용", + "dash.codexClientCompactionHint": "기본값은 꺼짐입니다. 새로운 OpenCodeX ocx1 상태 대신 이식 가능한 평문 요약을 저장합니다. V2 하위 에이전트 라우팅은 변경되지 않습니다. 기존 ocx1 기록은 별도로 복구해야 합니다. 변경 후 Codex를 다시 시작하세요.", "models.newPolicyGlobal": "새 모델을 비활성화 상태로 추가", "models.newPolicyProvider": "새 모델 정책", "models.newPolicy_inherit": "상속", "models.newPolicy_off": "끔", "models.newPolicy_on": "켬", "models.newBadge": "신규", "models.newCount": "신규 {count}개, 꺼짐", "models.aliases": "별칭", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index c583bccb99..19391715db 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -2509,6 +2509,8 @@ export const ru: Record = { "dash.visionAdvancedPopover": "Дополнительные настройки изображений", "dash.codexDesktopAuthless": "Открывать Codex без входа", "dash.codexDesktopAuthlessHint": "По умолчанию выключено. Пропускает отдельный вход в Desktop для допустимых локальных подключений. Учётные данные провайдера по-прежнему нужны. После изменения перезапустите Codex. Функции Desktop, связанные с аккаунтом, могут быть недоступны.", + "dash.codexClientCompaction": "Использовать сжатие на стороне клиента", + "dash.codexClientCompactionHint": "По умолчанию выключено. Сохраняет переносимые текстовые сводки вместо нового состояния OpenCodeX ocx1. Маршрутизация подагентов V2 не меняется. Существующую историю ocx1 всё равно нужно восстановить. После изменения перезапустите Codex.", "models.newPolicyGlobal": "Добавлять новые модели выключенными", "models.newPolicyProvider": "Политика новых моделей", "models.newPolicy_inherit": "Наследовать", "models.newPolicy_off": "Выкл.", "models.newPolicy_on": "Вкл.", "models.newBadge": "НОВАЯ", "models.newCount": "Новых: {count}, выкл.", "models.aliases": "Псевдонимы", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index ca39260677..0c7a5d03f6 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -2509,6 +2509,8 @@ export const tr: Record = { "dash.visionAdvancedPopover": "Gelişmiş görsel ayarları", "dash.codexDesktopAuthless": "Codex’i oturum açmadan başlat", "dash.codexDesktopAuthlessHint": "Varsayılan olarak kapalıdır. Uygun yerel bağlantılarda ayrı Desktop oturum açma adımını atlar. Sağlayıcı kimlik bilgileri yine gereklidir. Değişiklikten sonra Codex’i yeniden başlatın. Hesaba bağlı Desktop özellikleri kullanılamayabilir.", + "dash.codexClientCompaction": "İstemci tarafı sıkıştırmayı kullan", + "dash.codexClientCompactionHint": "Varsayılan olarak kapalıdır. Yeni OpenCodeX ocx1 durumu yerine taşınabilir düz metin özetleri kaydeder. V2 alt ajan yönlendirmesi değişmez. Mevcut ocx1 geçmişi yine ayrıca kurtarılmalıdır. Değişiklikten sonra Codex’i yeniden başlatın.", "models.newPolicyGlobal": "Yeni modeller devre dışı başlasın", "models.newPolicyProvider": "Yeni model ilkesi", "models.newPolicy_inherit": "Devral", "models.newPolicy_off": "Kapalı", "models.newPolicy_on": "Açık", "models.newBadge": "YENİ", "models.newCount": "{count} yeni, kapalı", "models.aliases": "Takma adlar", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 6462f6c4b5..76b48a978b 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -2471,6 +2471,8 @@ export const zhTW: Record = { "dash.visionAdvancedPopover": "進階視覺設定", "dash.codexDesktopAuthless": "無需登入即可開啟 Codex", "dash.codexDesktopAuthlessHint": "預設關閉。為符合條件的本機連線略過獨立的 Desktop 登入。仍需上游供應商憑證。變更後請重新啟動 Codex。依賴帳戶的 Desktop 功能可能無法使用。", + "dash.codexClientCompaction": "使用用戶端壓縮", + "dash.codexClientCompactionHint": "預設關閉。儲存可攜的純文字摘要,不再產生新的 OpenCodeX ocx1 狀態。V2 子代理路由不受影響。既有 ocx1 歷程仍須另行復原。變更後請重新啟動 Codex。", "models.newPolicyGlobal": "新模型預設停用", "models.newPolicyProvider": "新模型策略", "models.newPolicy_inherit": "繼承", "models.newPolicy_off": "關閉", "models.newPolicy_on": "開啟", "models.newBadge": "新增", "models.newCount": "{count} 個新增,已關閉", "models.aliases": "別名", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 685227abc0..3e4d044bbc 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -2507,6 +2507,8 @@ export const zh: Record = { "dash.visionAdvancedPopover": "高级视觉设置", "dash.codexDesktopAuthless": "无需登录即可打开 Codex", "dash.codexDesktopAuthlessHint": "默认关闭。为符合条件的本地连接跳过单独的 Desktop 登录。仍需上游提供商凭据。更改后请重启 Codex。依赖账户的 Desktop 功能可能不可用。", + "dash.codexClientCompaction": "使用客户端压缩", + "dash.codexClientCompactionHint": "默认关闭。保存可移植的明文摘要,不再生成新的 OpenCodeX ocx1 状态。V2 子代理路由不受影响。已有 ocx1 历史仍需单独恢复。更改后请重启 Codex。", "models.newPolicyGlobal": "新模型默认停用", "models.newPolicyProvider": "新模型策略", "models.newPolicy_inherit": "继承", "models.newPolicy_off": "关闭", "models.newPolicy_on": "开启", "models.newBadge": "新增", "models.newCount": "{count} 个新增,已关闭", "models.aliases": "别名", diff --git a/gui/src/pages/dashboard-overview-sections.tsx b/gui/src/pages/dashboard-overview-sections.tsx index 6606c4f560..2cc5d255b6 100644 --- a/gui/src/pages/dashboard-overview-sections.tsx +++ b/gui/src/pages/dashboard-overview-sections.tsx @@ -439,6 +439,7 @@ function VisionAdvancedPopover({ t, open, triggerRef, onClose, maxValue, maxInva export function DashboardSidecarPanels({ d }: { d: Dash }) { const { t, settings, settingsSaving, syncing, toggleCodexAutoStart, toggleCodexDesktopAuthless, + toggleCodexClientCompaction, sidecar, sidecarSaving, sidecarModels, visionModels, models, saveSidecar, shadowCall, shadowCallSaving, shadowCallHelpTriggerRef, shadowCallHelpOpen, setShadowCallHelpOpen, saveShadowCall, } = d; @@ -525,6 +526,25 @@ export function DashboardSidecarPanels({ d }: { d: Dash }) { +
+
+
+
{t("dash.codexClientCompaction")}
+
{t("dash.codexClientCompactionHint")}
+
+ +
+
+
{/* Both sidecar cards wear the DashboardInjectionPanel shell: the PANEL is the flex row, copy left, controls right. */} diff --git a/gui/src/pages/dashboard-shared.ts b/gui/src/pages/dashboard-shared.ts index b6914586b6..029e39b2da 100644 --- a/gui/src/pages/dashboard-shared.ts +++ b/gui/src/pages/dashboard-shared.ts @@ -49,6 +49,7 @@ export interface ModelInfo { id: string; provider: string; namespaced: string; o export interface SettingsData { codexAutoStart: boolean; codexDesktopAuthless?: boolean; + codexClientCompaction?: boolean; catalogRefreshPending?: boolean; /** Whether a login may open a browser on the machine running the proxy. */ oauthOpenBrowser?: boolean; diff --git a/gui/src/pages/use-dashboard-data.ts b/gui/src/pages/use-dashboard-data.ts index 605d4eefc4..5bbe1210bc 100644 --- a/gui/src/pages/use-dashboard-data.ts +++ b/gui/src/pages/use-dashboard-data.ts @@ -70,7 +70,7 @@ type CachedOverview = { type MaMode = "v1" | "default" | "v2"; -type CodexPreference = "codexAutoStart" | "codexDesktopAuthless"; +type CodexPreference = "codexAutoStart" | "codexDesktopAuthless" | "codexClientCompaction"; type DashboardSettingsState = { settings: SettingsData | null; beforeSave: SettingsData | null; @@ -106,7 +106,9 @@ function dashboardSettingsReducer(state: DashboardSettingsState, action: Dashboa settings: { ...state.settings, [action.key]: action.settings[action.key], - catalogRefreshPending: action.key === "codexDesktopAuthless" ? true : state.settings.catalogRefreshPending, + catalogRefreshPending: action.key === "codexDesktopAuthless" || action.key === "codexClientCompaction" + ? true + : state.settings.catalogRefreshPending, startupHealth: action.settings.startupHealth ?? state.settings.startupHealth, }, }; @@ -677,7 +679,7 @@ export function useDashboardData(apiBase: string) { const data = await requireJson(res, "save failed"); settingsMutationEpochRef.current += 1; dispatchSettings({ type: "save-succeeded", key, settings: data }); - if (key === "codexDesktopAuthless") await runSync(); + if (key === "codexDesktopAuthless" || key === "codexClientCompaction") await runSync(); } catch { dispatchSettings({ type: "save-failed" }); setError(true); @@ -689,6 +691,7 @@ export function useDashboardData(apiBase: string) { const toggleCodexAutoStart = () => toggleCodexSetting("codexAutoStart"); const toggleCodexDesktopAuthless = () => toggleCodexSetting("codexDesktopAuthless"); + const toggleCodexClientCompaction = () => toggleCodexSetting("codexClientCompaction"); // Clears the sync result/error in this hook. The dashboard toast owns its own dismissal // timer but must publish the dismissal here: syncResult/syncError live above the dashboard @@ -851,7 +854,8 @@ export function useDashboardData(apiBase: string) { effortCapHelpTriggerRef, updateTriggerRef, maHelpTriggerRef, shadowCallHelpTriggerRef, effortCapHelpDialogRef, updateDialogRef, maHelpDialogRef, shadowCallHelpDialogRef, filteredGroups, sidecarModels, visionModels, - saveSidecar, saveShadowCall, switchMaMode, toggleCodexAutoStart, toggleCodexDesktopAuthless, runSync, clearSyncFeedback, + saveSidecar, saveShadowCall, switchMaMode, toggleCodexAutoStart, toggleCodexDesktopAuthless, + toggleCodexClientCompaction, runSync, clearSyncFeedback, fetchUpdateCheck, closeUpdateDialog, openUpdateDialog, changeUpdateChannel, runUpdate, }; } diff --git a/gui/tests/vision-sidecar-dashboard.test.tsx b/gui/tests/vision-sidecar-dashboard.test.tsx index 37bcf40d65..4776398df9 100644 --- a/gui/tests/vision-sidecar-dashboard.test.tsx +++ b/gui/tests/vision-sidecar-dashboard.test.tsx @@ -409,6 +409,21 @@ test("Desktop login switch defaults off, preserves explicit opt-in, and disables expect(host.textContent).toContain(en["codexAuth.catalogRefreshPending"]); }); +test("client compaction switch defaults off, preserves explicit opt-in, and invokes its handler", async () => { + const { d } = harness(); + let clicks = 0; + d.toggleCodexClientCompaction = async () => { clicks += 1; }; + d.settings = { codexAutoStart: true, port: 10100, hostname: "127.0.0.1" }; + await mount(d); + const toggle = () => host.querySelector(`button[aria-label="${en["dash.codexClientCompaction"]}"]`)!; + expect(toggle().getAttribute("aria-pressed")).toBe("false"); + d.settings.codexClientCompaction = true; + await mount(d); + expect(toggle().getAttribute("aria-pressed")).toBe("true"); + await act(async () => { toggle().click(); }); + expect(clicks).toBe(1); +}); + test.each([undefined, false, true])("Desktop login preference %s persists before full sync; sync failure keeps the saved preference", async (initial) => { const originalFetch = globalThis.fetch; diff --git a/src/cli/system-command.ts b/src/cli/system-command.ts index 7811e7d432..06042fce10 100644 --- a/src/cli/system-command.ts +++ b/src/cli/system-command.ts @@ -14,7 +14,7 @@ import { const USAGE = `Usage: ocx system [status] [--json] ocx system settings [--auto-start ] [--stream-mode ] - [--desktop-authless ] [--json] + [--desktop-authless ] [--client-compaction ] [--json] ocx system startup [--json] ocx system diagnostics [--json] ocx system sync [--json] @@ -44,8 +44,10 @@ async function settings(argv: string[], deps: RuntimeApiDeps): Promise { const autoStart = takeBooleanOption(args, "--auto-start"); const streamMode = takeOption(args, "--stream-mode"); const desktopAuthless = takeBooleanOption(args, "--desktop-authless"); + const clientCompaction = takeBooleanOption(args, "--client-compaction"); rejectArgs(args, USAGE); - if (autoStart === undefined && streamMode === undefined && desktopAuthless === undefined) { + if (autoStart === undefined && streamMode === undefined + && desktopAuthless === undefined && clientCompaction === undefined) { const result = await runtimeRequest("/api/settings", {}, deps); printData(result, wantsJson, summaryLines(result)); return; @@ -54,6 +56,7 @@ async function settings(argv: string[], deps: RuntimeApiDeps): Promise { ...(autoStart !== undefined ? { codexAutoStart: autoStart } : {}), ...(streamMode !== undefined ? { streamMode } : {}), ...(desktopAuthless !== undefined ? { codexDesktopAuthless: desktopAuthless } : {}), + ...(clientCompaction !== undefined ? { codexClientCompaction: clientCompaction } : {}), }; const result = await runtimeRequest("/api/settings", { method: "PUT", body: JSON.stringify(body) }, deps); printData(result, wantsJson, ["System settings updated."]); diff --git a/src/codex/inject.ts b/src/codex/inject.ts index 2241d8422b..54e4030464 100644 --- a/src/codex/inject.ts +++ b/src/codex/inject.ts @@ -175,6 +175,8 @@ export interface CodexRoutingTarget { * and is never weakened by this flag. */ desktopAuthless?: boolean; + /** Select the dedicated provider identity so Codex owns compaction locally. */ + clientCompaction?: boolean; } function validateCodexRoutingTarget(target: CodexRoutingTarget): CodexRoutingTarget { @@ -198,14 +200,19 @@ function validateCodexRoutingTarget(target: CodexRoutingTarget): CodexRoutingTar return { ...target, baseUrl: `${parsed.origin}/v1` }; } -/** Provider-table form is used for non-loopback admission and for the authless Desktop opt-in. */ +/** Provider-table form is used when auth, admission, or compaction policy needs a dedicated provider. */ function usesProviderTable(target: CodexRoutingTarget): boolean { - return target.requiresAdmissionToken || target.desktopAuthless === true; + return target.requiresAdmissionToken + || target.desktopAuthless === true + || target.clientCompaction === true; } export function standaloneCodexRoutingTarget( port: number, - config?: Pick, + config?: Pick< + OcxConfig, + "hostname" | "unauthenticatedLoopbackListener" | "codexDesktopAuthless" | "codexClientCompaction" + >, ): CodexRoutingTarget { const loopback = config?.unauthenticatedLoopbackListener; const effectivePort = loopback?.enabled ? loopback.port : port; @@ -218,6 +225,9 @@ export function standaloneCodexRoutingTarget( ...(config?.codexDesktopAuthless === true && !requiresAdmissionToken ? { desktopAuthless: true } : {}), + ...(config?.codexClientCompaction === true && !requiresAdmissionToken + ? { clientCompaction: true } + : {}), }; } @@ -834,7 +844,7 @@ function buildProfileFileForTarget( const host = new URL(origin).host; // Design B (loopback): the reference/fallback file documents the root override form. // Non-loopback keeps the legacy provider-table shape (built-in provider cannot carry - // the x-opencodex-api-key env header); the authless Desktop opt-in shares that shape. + // the x-opencodex-api-key env header); explicit Desktop policies share that shape. if (!usesProviderTable(target)) { const lines = [ "# OpenCodex proxy fallback config (Design B)", @@ -1015,7 +1025,7 @@ export async function injectCodexConfig( ? setRootModelCatalogPath(content, catalogPath) : stripOpencodexCatalogPath(content); - // Provider-table form: non-loopback admission (legacy) or the authless Desktop opt-in (#1107). + // Provider-table form: non-loopback admission or an explicit Desktop policy. const legacyMode = usesProviderTable(routingTarget); let keptUserBaseUrl = false; let keptUserRealtimeWsBaseUrl = false; @@ -1403,6 +1413,8 @@ export async function injectCodexConfig( } const headline = routingTarget.desktopAuthless === true ? `Injected opencodex as default provider into Codex config (authless Desktop mode: requires_openai_auth = false).\n` + : routingTarget.clientCompaction === true + ? `Injected opencodex as default provider into Codex config (client-side compaction mode; ChatGPT auth remains required).\n` : legacyMode ? `Injected opencodex as default provider into Codex config.\n` : `Pointed Codex's built-in openai provider at the opencodex proxy (openai_base_url + realtime sideband override).\n`; diff --git a/src/config.ts b/src/config.ts index 8da89cbfdf..8cfaf63391 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1189,6 +1189,7 @@ const configSchema = z.object({ ).optional().catch(undefined), codexShimAutoRestore: z.boolean().optional(), codexDesktopAuthless: z.boolean().optional().catch(undefined), + codexClientCompaction: z.boolean().optional().catch(undefined), pausedCodexAccountIds: z.array(z.string().regex(/^[a-zA-Z0-9._-]{1,64}$/)).optional(), codexQuotaAutoRefresh: codexQuotaAutoRefreshSchema.optional().catch(undefined), codexAccountNamespaces: codexAccountNamespacesSchema.optional(), diff --git a/src/server/management/config-routes.ts b/src/server/management/config-routes.ts index 08f4b85d27..527178ba0f 100644 --- a/src/server/management/config-routes.ts +++ b/src/server/management/config-routes.ts @@ -329,6 +329,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise> = []; +describe("ocx system settings client compaction", () => { + test("persists the explicit boolean through the shared settings endpoint", async () => { + const { requests, deps } = fakeRuntime((_req, body) => ({ ok: true, ...body })); + const logSpy = spyOn(console, "log").mockImplementation(() => {}); + try { + expect(await handleSystemCommand(["settings", "--client-compaction", "on"], deps)).toBe(0); + expect(requests).toEqual([{ + path: "/api/settings", + method: "PUT", + body: { codexClientCompaction: true }, + }]); + } finally { + logSpy.mockRestore(); + } + }); +}); + describe("ocx agent sidecar --list (#2188)", () => { test("web --list prints the server's webSearchModels — the GUI's exact list", async () => { const { requests, deps } = fakeRuntime(req => { diff --git a/tests/codex-integration/codex-inject-integration.test.ts b/tests/codex-integration/codex-inject-integration.test.ts index 3a5345098e..bf9944a097 100644 --- a/tests/codex-integration/codex-inject-integration.test.ts +++ b/tests/codex-integration/codex-inject-integration.test.ts @@ -879,6 +879,26 @@ describe("injectCodexConfig integration (Design B)", () => { expect(restored).toContain('model = "gpt-5.5"'); }); + test("client compaction opt-in (#3978): writes an authenticated provider table and returns to Design B", () => { + writeFileSync(join(codexHome, "config.toml"), 'model = "gpt-5.5"\n', "utf8"); + + const enabled = runInject(codexHome, ocxHome, JSON.stringify({ codexClientCompaction: true })); + expect(enabled.status).toBe(0); + expect(String(JSON.parse(enabled.stdout).message)).toContain("client-side compaction mode"); + const providerTable = readFileSync(join(codexHome, "config.toml"), "utf8"); + expect(providerTable).toContain('model_provider = "opencodex"'); + expect(providerTable).toContain("[model_providers.opencodex]"); + expect(providerTable).toContain("requires_openai_auth = true"); + expect(providerTable).not.toContain("requires_openai_auth = false"); + expect(providerTable).not.toContain("openai_base_url"); + + expect(runInject(codexHome, ocxHome).status).toBe(0); + const designB = readFileSync(join(codexHome, "config.toml"), "utf8"); + expect(designB).toContain(DESIGN_B_BLOCK); + expect(designB).not.toContain("[model_providers.opencodex]"); + expect(designB).not.toContain('model_provider = "opencodex"'); + }); + test("authless Desktop opt-in never weakens non-loopback admission", () => { writeFileSync(join(codexHome, "config.toml"), 'model = "gpt-5.5"\n', "utf8"); diff --git a/tests/codex-integration/codex-inject.test.ts b/tests/codex-integration/codex-inject.test.ts index b6be3c2f69..92927cfc3d 100644 --- a/tests/codex-integration/codex-inject.test.ts +++ b/tests/codex-integration/codex-inject.test.ts @@ -70,6 +70,49 @@ describe("Codex config injection", () => { }); }); + describe("Codex client compaction opt-in (#3978)", () => { + test.each([undefined, false])("disabled preference %s keeps authenticated loopback on Design B", (codexClientCompaction) => { + const target = standaloneCodexRoutingTarget(10100, { codexClientCompaction }); + expect(target.clientCompaction).toBeUndefined(); + expect(buildProfileFile(target, null)).toBe(buildProfileFile(10100, null)); + }); + + test("loopback opt-in selects the dedicated provider without disabling ChatGPT auth", () => { + const target = standaloneCodexRoutingTarget(10100, { codexClientCompaction: true }); + expect(target).toMatchObject({ + requiresAdmissionToken: false, + clientCompaction: true, + }); + expect(target.desktopAuthless).toBeUndefined(); + + const profile = buildProfileFile(target, "/tmp/opencodex-catalog.json"); + expect(profile).toContain('model_provider = "opencodex"'); + expect(profile).toContain("requires_openai_auth = true"); + expect(profile).not.toContain("openai_base_url"); + }); + + test("authless remains the stronger provider-table policy when both preferences are enabled", () => { + const target = standaloneCodexRoutingTarget(10100, { + codexClientCompaction: true, + codexDesktopAuthless: true, + }); + const profile = buildProfileFile(target, null); + expect(profile).toContain('model_provider = "opencodex"'); + expect(profile).toContain("requires_openai_auth = false"); + }); + + test("non-loopback admission remains token-protected", () => { + const target = standaloneCodexRoutingTarget(10100, { + hostname: "192.168.1.20", + codexClientCompaction: true, + }); + expect(target.requiresAdmissionToken).toBe(true); + const profile = buildProfileFile(target, null); + expect(profile).toContain('env_key = "OPENCODEX_API_AUTH_TOKEN"'); + expect(profile).toContain("requires_openai_auth = true"); + }); + }); + test("explicit HTTPS target emits exact provider destination and admission env", () => { const target = { baseUrl: "https://hub.example.test/v1", diff --git a/tests/config/settings-stream-mode.test.ts b/tests/config/settings-stream-mode.test.ts index 272e45be6a..514ba6110a 100644 --- a/tests/config/settings-stream-mode.test.ts +++ b/tests/config/settings-stream-mode.test.ts @@ -379,6 +379,42 @@ describe("PUT /api/settings", () => { expect(bad!.status).toBe(400); }); + test("codexClientCompaction (#3978): absent reports false, changes converge once, and disable deletes the key", async () => { + const config = baseConfig(); + const absent = await (await getSettings(config))!.json() as { codexClientCompaction?: boolean }; + expect(absent.codexClientCompaction).toBe(false); + + let convergences = 0; + let saved: OcxConfig | undefined; + const on = await putSettings(config, { codexClientCompaction: true }, { + saveConfigPreservingClaudeCode: next => { saved = next; }, + createManagementConvergeCodex: catalogConvergenceFactory(() => { convergences += 1; }), + }); + expect(on!.status).toBe(200); + expect(await on!.json()).toMatchObject({ codexClientCompaction: true }); + expect(saved?.codexClientCompaction).toBe(true); + expect(convergences).toBe(1); + + const same = await putSettings(config, { codexClientCompaction: true }, { + saveConfigPreservingClaudeCode: () => {}, + createManagementConvergeCodex: catalogConvergenceFactory(() => { convergences += 1; }), + }); + expect(same!.status).toBe(200); + expect(convergences).toBe(1); + + const off = await putSettings(config, { codexClientCompaction: false }, { + saveConfigPreservingClaudeCode: next => { saved = next; }, + createManagementConvergeCodex: catalogConvergenceFactory(() => { convergences += 1; }), + }); + expect(off!.status).toBe(200); + expect(await off!.json()).toMatchObject({ codexClientCompaction: false }); + expect(Object.hasOwn(saved!, "codexClientCompaction")).toBe(false); + expect(convergences).toBe(2); + + const bad = await putSettings(config, { codexClientCompaction: "yes" }); + expect(bad!.status).toBe(400); + }); + test("account-picker disable does not initialize an empty namespace map", async () => { const config = baseConfig(); let convergences = 0; From 3c3307e6d9029704b6db84a966ed19a63d8d631b Mon Sep 17 00:00:00 2001 From: R <53855466+cb8010d6@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:12:02 +0800 Subject: [PATCH 2/8] test(gui): preserve client compaction on sync failure --- gui/tests/vision-sidecar-dashboard.test.tsx | 54 +++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/gui/tests/vision-sidecar-dashboard.test.tsx b/gui/tests/vision-sidecar-dashboard.test.tsx index 4776398df9..4a6193b979 100644 --- a/gui/tests/vision-sidecar-dashboard.test.tsx +++ b/gui/tests/vision-sidecar-dashboard.test.tsx @@ -424,6 +424,60 @@ test("client compaction switch defaults off, preserves explicit opt-in, and invo expect(clicks).toBe(1); }); +test("client compaction preference survives a successful save followed by sync failure", async () => { + const originalFetch = globalThis.fetch; + const writes: Array<{ path: string; body: unknown }> = []; + let latest: Dash | undefined; + let saved = false; + const apiBase = "/client-compaction-sync-failure"; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const path = String(input); + if (path.endsWith("/api/settings")) { + if (init?.method === "PUT") { + const body = JSON.parse(String(init.body)); + writes.push({ path, body }); + saved = body.codexClientCompaction; + return Response.json({ codexClientCompaction: saved, catalogRefreshPending: true }); + } + return Response.json({ + codexAutoStart: true, + codexClientCompaction: saved, + port: 10100, + hostname: "127.0.0.1", + }); + } + if (path.endsWith("/api/sync")) { + writes.push({ path, body: null }); + return Response.json({ error: "sync unavailable" }, { status: 503 }); + } + return Response.json({}, { status: 503 }); + }) as typeof fetch; + function Harness() { + const data = useDashboardData(apiBase); + useEffect(() => { latest = data; }, [data]); + return null; + } + try { + const { createRoot } = await import("react-dom/client"); + await act(async () => { + root = createRoot(host); + root.render(); + }); + await act(async () => { await latest!.toggleCodexClientCompaction(); }); + expect(writes).toEqual([ + { path: `${apiBase}/api/settings`, body: { codexClientCompaction: true } }, + { path: `${apiBase}/api/sync`, body: null }, + ]); + expect(latest?.settings?.codexClientCompaction).toBe(true); + expect(latest?.settings?.catalogRefreshPending).toBe(true); + expect(latest?.syncError).toBe("sync unavailable"); + } finally { + await act(async () => { root?.unmount(); }); + root = null; + globalThis.fetch = originalFetch; + } +}); + test.each([undefined, false, true])("Desktop login preference %s persists before full sync; sync failure keeps the saved preference", async (initial) => { const originalFetch = globalThis.fetch; From cc49bd61c3a81bf0cc5048368eed55b927bdbfe6 Mon Sep 17 00:00:00 2001 From: R <53855466+cb8010d6@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:30:59 +0800 Subject: [PATCH 3/8] fix(codex): keep client compaction future-only --- .../content/docs/guides/codex-integration.md | 16 +++++---- .../docs/reference/configuration/server.md | 2 +- gui/src/i18n/de.ts | 2 +- gui/src/i18n/en.ts | 2 +- gui/src/i18n/fr.ts | 2 +- gui/src/i18n/ja.ts | 2 +- gui/src/i18n/ko.ts | 2 +- gui/src/i18n/ru.ts | 2 +- gui/src/i18n/tr.ts | 2 +- gui/src/i18n/zh-TW.ts | 2 +- gui/src/i18n/zh.ts | 2 +- gui/src/pages/dashboard-overview-sections.tsx | 1 + src/cli/system-command.ts | 6 +++- src/codex/inject.ts | 31 ++++++++++------- .../codex-inject-integration.test.ts | 33 +++++++++++++++++++ 15 files changed, 77 insertions(+), 30 deletions(-) diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index ee5ffb4da3..bb839fe21e 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -260,8 +260,10 @@ ocx system settings --client-compaction on # or "codexClientCompaction": true ocx sync # rewrites ~/.codex/config.toml; restart Desktop ``` -The setting defaults to off. When enabled, OpenCodeX selects its existing dedicated provider form -with `requires_openai_auth = true`: +The setting defaults to off. For authenticated loopback routing, OpenCodeX selects its existing +dedicated provider form with `requires_openai_auth = true`. If `codexDesktopAuthless` is also +enabled, that stronger compatibility setting takes precedence and writes +`requires_openai_auth = false`: ```toml model_provider = "opencodex" @@ -279,11 +281,11 @@ selected provider. Provider-level V2 policy is independent: plaintext delivery, passthrough through `allowEncryptedV2AgentTasks`, and configured recovery or fallback behavior do not change. -This preference affects future compactions only. It does not rewrite existing `ocx1:` history; -use the explicit history recovery workflow for an affected thread. New threads use the -`opencodex` provider identity while the mode is active, so the existing resume-history compatibility -and restore behavior applies. Turning the setting off and syncing restores the default Design B -root override. +This preference affects future compactions only. It does not rewrite existing `ocx1:` history or +re-tag existing resume-history metadata; use the explicit history recovery workflow for an affected +thread. New threads use the `opencodex` provider identity while the mode is active. Turning the +setting off and syncing restores the default Design B root override unless +`codexDesktopAuthless` or non-loopback admission still requires the provider-table form. ### Authless Codex Desktop (opt-in) diff --git a/docs-site/src/content/docs/reference/configuration/server.md b/docs-site/src/content/docs/reference/configuration/server.md index 79dc6d1299..0fe874c9a9 100644 --- a/docs-site/src/content/docs/reference/configuration/server.md +++ b/docs-site/src/content/docs/reference/configuration/server.md @@ -27,7 +27,7 @@ runs helper features around provider requests. | `codexAutoStart?` | `boolean` | `true` | Let the Codex shim run `ocx ensure` before launching Codex. False makes ensure a no-op. | | `codexShimAutoRestore?` | `boolean` | `true` | Restore an installed shim after a completed external Codex update replaces it. Environment opt-out: `OPENCODEX_CODEX_SHIM_AUTO_RESTORE=0`. | | `codexDesktopAuthless?` | `boolean` | `false` | Opt-in authless Codex Desktop routing on a loopback bind: inject the dedicated `opencodex` provider with `requires_openai_auth = false` so Desktop opens without a ChatGPT login. Ignored on non-loopback binds. `ocx system settings --desktop-authless on`. See [Codex integration](/guides/codex-integration/#authless-codex-desktop-opt-in). | -| `codexClientCompaction?` | `boolean` | `false` | Opt into Codex client-side compaction on an authenticated loopback bind. Uses the dedicated `opencodex` provider identity with `requires_openai_auth = true`, preventing new routed compactions from storing OpenCodeX-owned `ocx1:` state. V2 sub-agent routing is unchanged. `ocx system settings --client-compaction on`. See [Codex integration](/guides/codex-integration/#client-side-compaction-opt-in). | +| `codexClientCompaction?` | `boolean` | `false` | Opt into Codex client-side compaction on an authenticated loopback bind. Uses the dedicated `opencodex` provider identity with `requires_openai_auth = true`, preventing new routed compactions from storing OpenCodeX-owned `ocx1:` state. `codexDesktopAuthless` takes precedence when both are enabled and keeps `requires_openai_auth = false`. V2 sub-agent routing is unchanged. `ocx system settings --client-compaction on`. See [Codex integration](/guides/codex-integration/#client-side-compaction-opt-in). | | `resetCreditAutoRedeem?` | `{ enabled?: boolean; leadTimeMinutes?: number }` | off | Opt-in: redeem the main Codex account's soonest-expiring reset credit `leadTimeMinutes` (1–60, default 10) before it expires. Every attempt re-reads the upstream credit list first and skips when the credit is gone (for example, redeemed by hand); the `redeem_request_id` is journaled in `$OPENCODEX_HOME/reset-credit-auto-redeem.json` before the call so a crash replays the same idempotent request instead of spending a second credit. Servers sharing this configuration directory coordinate reservations and settlements so one process does not replace another's request record. Logs carry a hashed account key only. | | `syncResumeHistory?` | `boolean` | `true` | Reversible Codex App history compatibility. Original metadata is backed up and restored by `ocx stop` / `ocx restore`. | | `shadowCallIntercept?` | `{ enabled?: boolean; model?: string; sourceModels?: string[] }` | off | Redirect recognized Codex helper/shadow calls to a chosen model while preserving the request's configured reasoning effort. The default source prefix is `gpt-5.6-luna`; older clients through 0.144.x used `gpt-5.4-mini`, which `sourceModels` can restore. | diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 86a299bd8c..3821741e6d 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -2487,7 +2487,7 @@ export const de: Record = { "dash.codexDesktopAuthless": "Codex ohne Anmeldung öffnen", "dash.codexDesktopAuthlessHint": "Standardmäßig aus. Überspringt die separate Desktop-Anmeldung bei geeigneten lokalen Verbindungen. Zugangsdaten für den Anbieter bleiben erforderlich. Codex nach einer Änderung neu starten. Kontogebundene Desktop-Funktionen können fehlen.", "dash.codexClientCompaction": "Clientseitige Komprimierung verwenden", - "dash.codexClientCompactionHint": "Standardmäßig aus. Speichert portable Klartext-Zusammenfassungen statt neuer OpenCodeX-ocx1-Daten. Das V2-Subagent-Routing bleibt unverändert. Vorhandene ocx1-Verläufe müssen weiterhin wiederhergestellt werden. Codex nach einer Änderung neu starten.", + "dash.codexClientCompactionHint": "Standardmäßig aus; nur für authentifiziertes Loopback-Routing. Künftige Komprimierungen speichern portable Klartext-Zusammenfassungen, während OpenCodeX- und V2-Provider-Routing aktiv bleiben; der konfigurierte Anbieter kann sie verarbeiten und Kontingent verbrauchen. Vorhandene ocx1-Verläufe müssen weiterhin wiederhergestellt werden. Codex nach einer Änderung neu starten.", "models.newPolicyGlobal": "Neue Modelle zunächst deaktivieren", "models.newPolicyProvider": "Richtlinie für neue Modelle", "models.newPolicy_inherit": "Übernehmen", "models.newPolicy_off": "Aus", "models.newPolicy_on": "An", "models.newBadge": "NEU", "models.newCount": "{count} neu, aus", "models.aliases": "Aliase", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index af0c2dbf7f..851c38ce79 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -580,7 +580,7 @@ export const en = { "dash.codexDesktopAuthless": "Open Codex without signing in", "dash.codexDesktopAuthlessHint": "Off by default. Skip the separate Desktop sign-in for eligible local connections. Upstream credentials are still required. Restart Codex after changing this setting. Account-gated Desktop features may be unavailable.", "dash.codexClientCompaction": "Use client-side compaction", - "dash.codexClientCompactionHint": "Off by default. Store portable plaintext summaries instead of new OpenCodeX ocx1 state. V2 sub-agent routing is unchanged. Existing ocx1 history still needs recovery. Restart Codex after changing this setting.", + "dash.codexClientCompactionHint": "Off by default; authenticated loopback only. Future compactions store portable plaintext summaries while OpenCodeX and V2 provider routing stay active; the configured provider may process them and consume quota. Existing ocx1 history still needs recovery. Restart Codex after changing this setting.", "models.v2Conflict": "[agents] max_threads is set — codex will refuse to start; remove it from config.toml", "models.v2Applied": "Sub-agent mode updated — applies to new sessions (restart the Codex app to refresh the picker)", "models.v2ThreadsLabel": "Max threads", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 7e85aca05b..1ba6d0331b 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -565,7 +565,7 @@ export const fr: Record = { "dash.codexDesktopAuthless": "Ouvrir Codex sans se connecter", "dash.codexDesktopAuthlessHint": "Désactivé par défaut. Ignore la connexion Desktop séparée pour les connexions locales admissibles. Les identifiants du fournisseur restent nécessaires. Redémarrez Codex après toute modification. Certaines fonctions Desktop liées au compte peuvent être indisponibles.", "dash.codexClientCompaction": "Utiliser la compaction côté client", - "dash.codexClientCompactionHint": "Désactivé par défaut. Enregistre des résumés portables en texte clair au lieu de nouvelles données OpenCodeX ocx1. Le routage des sous-agents V2 ne change pas. L'historique ocx1 existant doit toujours être restauré. Redémarrez Codex après modification.", + "dash.codexClientCompactionHint": "Désactivé par défaut, uniquement pour le routage loopback authentifié. Les compactages futurs stockent des résumés portables en texte clair tout en conservant le routage OpenCodeX/V2 ; le fournisseur configuré peut les traiter et consommer son quota. L'historique ocx1 existant doit toujours être restauré. Redémarrez Codex après modification.", "models.v2Conflict": "[agents] max_threads est défini — codex refusera de démarrer ; supprimez-le de config.toml", "models.v2Applied": "Mode sous-agent mis à jour — s’applique aux nouvelles sessions (redémarrez l’application Codex pour actualiser le sélecteur)", "models.v2ThreadsLabel": "Nombre maximal de fils", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index cb0aa08a52..278c580bc7 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -2508,7 +2508,7 @@ export const ja: Record = { "dash.codexDesktopAuthless": "ログインせずに Codex を開く", "dash.codexDesktopAuthlessHint": "既定ではオフです。対象のローカル接続で Desktop の個別ログインを省略します。上流プロバイダーの認証情報は引き続き必要です。変更後は Codex を再起動してください。アカウントに依存する Desktop 機能が利用できない場合があります。", "dash.codexClientCompaction": "クライアント側コンパクションを使用", - "dash.codexClientCompactionHint": "既定ではオフです。新しい OpenCodeX ocx1 状態ではなく、移植可能な平文要約を保存します。V2 サブエージェントのルーティングは変わりません。既存の ocx1 履歴は別途復旧が必要です。変更後は Codex を再起動してください。", + "dash.codexClientCompactionHint": "既定ではオフで、認証済みループバックルーティング専用です。今後のコンパクションは、OpenCodeX と V2 プロバイダーのルーティングを維持したまま移植可能な平文要約を保存します。設定済みプロバイダーが要約を処理し、割り当てを消費する場合があります。既存の ocx1 履歴は別途復旧が必要です。変更後は Codex を再起動してください。", "models.newPolicyGlobal": "新しいモデルを無効で追加", "models.newPolicyProvider": "新しいモデルのポリシー", "models.newPolicy_inherit": "継承", "models.newPolicy_off": "オフ", "models.newPolicy_on": "オン", "models.newBadge": "新着", "models.newCount": "新着 {count} 件、オフ", "models.aliases": "エイリアス", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 58830a885a..ead3bd74e7 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -2509,7 +2509,7 @@ export const ko: Record = { "dash.codexDesktopAuthless": "로그인 없이 Codex 열기", "dash.codexDesktopAuthlessHint": "기본값은 꺼짐입니다. 지원되는 로컬 연결에서 별도의 Desktop 로그인을 건너뜁니다. 업스트림 인증 정보는 여전히 필요합니다. 변경 후 Codex를 다시 시작하세요. 계정에 연결된 Desktop 기능을 사용하지 못할 수 있습니다.", "dash.codexClientCompaction": "클라이언트 측 컴팩션 사용", - "dash.codexClientCompactionHint": "기본값은 꺼짐입니다. 새로운 OpenCodeX ocx1 상태 대신 이식 가능한 평문 요약을 저장합니다. V2 하위 에이전트 라우팅은 변경되지 않습니다. 기존 ocx1 기록은 별도로 복구해야 합니다. 변경 후 Codex를 다시 시작하세요.", + "dash.codexClientCompactionHint": "기본값은 꺼짐이며 인증된 루프백 라우팅에만 적용됩니다. 향후 컴팩션은 OpenCodeX 및 V2 제공자 라우팅을 유지하면서 이식 가능한 평문 요약을 저장합니다. 설정된 제공자가 요약을 처리하고 할당량을 사용할 수 있습니다. 기존 ocx1 기록은 별도로 복구해야 합니다. 변경 후 Codex를 다시 시작하세요.", "models.newPolicyGlobal": "새 모델을 비활성화 상태로 추가", "models.newPolicyProvider": "새 모델 정책", "models.newPolicy_inherit": "상속", "models.newPolicy_off": "끔", "models.newPolicy_on": "켬", "models.newBadge": "신규", "models.newCount": "신규 {count}개, 꺼짐", "models.aliases": "별칭", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 19391715db..72678d0579 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -2510,7 +2510,7 @@ export const ru: Record = { "dash.codexDesktopAuthless": "Открывать Codex без входа", "dash.codexDesktopAuthlessHint": "По умолчанию выключено. Пропускает отдельный вход в Desktop для допустимых локальных подключений. Учётные данные провайдера по-прежнему нужны. После изменения перезапустите Codex. Функции Desktop, связанные с аккаунтом, могут быть недоступны.", "dash.codexClientCompaction": "Использовать сжатие на стороне клиента", - "dash.codexClientCompactionHint": "По умолчанию выключено. Сохраняет переносимые текстовые сводки вместо нового состояния OpenCodeX ocx1. Маршрутизация подагентов V2 не меняется. Существующую историю ocx1 всё равно нужно восстановить. После изменения перезапустите Codex.", + "dash.codexClientCompactionHint": "По умолчанию выключено; только для аутентифицированной loopback-маршрутизации. Будущие сжатия сохраняют переносимые текстовые сводки, а маршрутизация OpenCodeX и V2 остаётся активной; настроенный провайдер может обрабатывать сводки и расходовать квоту. Существующую историю ocx1 всё равно нужно восстановить. После изменения перезапустите Codex.", "models.newPolicyGlobal": "Добавлять новые модели выключенными", "models.newPolicyProvider": "Политика новых моделей", "models.newPolicy_inherit": "Наследовать", "models.newPolicy_off": "Выкл.", "models.newPolicy_on": "Вкл.", "models.newBadge": "НОВАЯ", "models.newCount": "Новых: {count}, выкл.", "models.aliases": "Псевдонимы", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 0c7a5d03f6..ffa0272c2b 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -2510,7 +2510,7 @@ export const tr: Record = { "dash.codexDesktopAuthless": "Codex’i oturum açmadan başlat", "dash.codexDesktopAuthlessHint": "Varsayılan olarak kapalıdır. Uygun yerel bağlantılarda ayrı Desktop oturum açma adımını atlar. Sağlayıcı kimlik bilgileri yine gereklidir. Değişiklikten sonra Codex’i yeniden başlatın. Hesaba bağlı Desktop özellikleri kullanılamayabilir.", "dash.codexClientCompaction": "İstemci tarafı sıkıştırmayı kullan", - "dash.codexClientCompactionHint": "Varsayılan olarak kapalıdır. Yeni OpenCodeX ocx1 durumu yerine taşınabilir düz metin özetleri kaydeder. V2 alt ajan yönlendirmesi değişmez. Mevcut ocx1 geçmişi yine ayrıca kurtarılmalıdır. Değişiklikten sonra Codex’i yeniden başlatın.", + "dash.codexClientCompactionHint": "Varsayılan olarak kapalıdır ve yalnızca kimliği doğrulanmış geri döngü yönlendirmesinde geçerlidir. Gelecekteki sıkıştırmalar, OpenCodeX ve V2 sağlayıcı yönlendirmesi etkin kalırken taşınabilir düz metin özetleri kaydeder; yapılandırılmış sağlayıcı bunları işleyip kotasını tüketebilir. Mevcut ocx1 geçmişi yine ayrıca kurtarılmalıdır. Değişiklikten sonra Codex’i yeniden başlatın.", "models.newPolicyGlobal": "Yeni modeller devre dışı başlasın", "models.newPolicyProvider": "Yeni model ilkesi", "models.newPolicy_inherit": "Devral", "models.newPolicy_off": "Kapalı", "models.newPolicy_on": "Açık", "models.newBadge": "YENİ", "models.newCount": "{count} yeni, kapalı", "models.aliases": "Takma adlar", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 76b48a978b..0c346f2375 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -2472,7 +2472,7 @@ export const zhTW: Record = { "dash.codexDesktopAuthless": "無需登入即可開啟 Codex", "dash.codexDesktopAuthlessHint": "預設關閉。為符合條件的本機連線略過獨立的 Desktop 登入。仍需上游供應商憑證。變更後請重新啟動 Codex。依賴帳戶的 Desktop 功能可能無法使用。", "dash.codexClientCompaction": "使用用戶端壓縮", - "dash.codexClientCompactionHint": "預設關閉。儲存可攜的純文字摘要,不再產生新的 OpenCodeX ocx1 狀態。V2 子代理路由不受影響。既有 ocx1 歷程仍須另行復原。變更後請重新啟動 Codex。", + "dash.codexClientCompactionHint": "預設關閉,僅適用於已驗證的 loopback 路由。未來壓縮會儲存可攜的純文字摘要,同時保留 OpenCodeX 與 V2 提供方路由;已設定的提供方可能處理摘要並消耗其額度。既有 ocx1 歷程仍須另行復原。變更後請重新啟動 Codex。", "models.newPolicyGlobal": "新模型預設停用", "models.newPolicyProvider": "新模型策略", "models.newPolicy_inherit": "繼承", "models.newPolicy_off": "關閉", "models.newPolicy_on": "開啟", "models.newBadge": "新增", "models.newCount": "{count} 個新增,已關閉", "models.aliases": "別名", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 3e4d044bbc..7b97cc0a9b 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -2508,7 +2508,7 @@ export const zh: Record = { "dash.codexDesktopAuthless": "无需登录即可打开 Codex", "dash.codexDesktopAuthlessHint": "默认关闭。为符合条件的本地连接跳过单独的 Desktop 登录。仍需上游提供商凭据。更改后请重启 Codex。依赖账户的 Desktop 功能可能不可用。", "dash.codexClientCompaction": "使用客户端压缩", - "dash.codexClientCompactionHint": "默认关闭。保存可移植的明文摘要,不再生成新的 OpenCodeX ocx1 状态。V2 子代理路由不受影响。已有 ocx1 历史仍需单独恢复。更改后请重启 Codex。", + "dash.codexClientCompactionHint": "默认关闭,仅适用于已认证的 loopback 路由。未来压缩会保存可移植的明文摘要,同时保留 OpenCodeX 与 V2 提供方路由;已配置的提供方可能处理摘要并消耗其额度。已有 ocx1 历史仍需单独恢复。更改后请重启 Codex。", "models.newPolicyGlobal": "新模型默认停用", "models.newPolicyProvider": "新模型策略", "models.newPolicy_inherit": "继承", "models.newPolicy_off": "关闭", "models.newPolicy_on": "开启", "models.newBadge": "新增", "models.newCount": "{count} 个新增,已关闭", "models.aliases": "别名", diff --git a/gui/src/pages/dashboard-overview-sections.tsx b/gui/src/pages/dashboard-overview-sections.tsx index 2cc5d255b6..c71687acb9 100644 --- a/gui/src/pages/dashboard-overview-sections.tsx +++ b/gui/src/pages/dashboard-overview-sections.tsx @@ -531,6 +531,7 @@ export function DashboardSidecarPanels({ d }: { d: Dash }) {
{t("dash.codexClientCompaction")}
{t("dash.codexClientCompactionHint")}
+ {settings?.catalogRefreshPending &&
{t("codexAuth.catalogRefreshPending")}
}