From bb42ca1f992beb32ffde0c527e778624f5706b75 Mon Sep 17 00:00:00 2001 From: Julian <148066842+JulianZJN@users.noreply.github.com> Date: Mon, 14 Sep 2026 02:21:06 +0800 Subject: [PATCH 1/4] feat: select a separate model for Pro compaction --- README.ja.md | 5 + README.md | 4 + README.zh-CN.md | 4 + docs/architecture.md | 6 + docs/compaction-model-selection.md | 71 +++ launcher/electron/main.cjs | 12 + launcher/electron/preload.cjs | 1 + launcher/electron/runtime-supervisor.cjs | 6 + launcher/electron/runtime.cjs | 37 ++ launcher/src/App.tsx | 58 +++ launcher/src/i18n.ts | 21 + launcher/src/styles.css | 22 + launcher/src/types.ts | 3 + launcher/tests/compaction-model.test.cjs | 153 ++++++ launcher/tests/runtime-supervisor.test.cjs | 12 + .../chatgpt-web/browser-helper-main.ts | 28 +- src/adapters/chatgpt-web/browser-worker.ts | 169 ++++++- .../chatgpt-web/compaction-handoff.ts | 3 + src/adapters/chatgpt-web/index.ts | 56 ++- .../chatgpt-web/launcher-helper-client.ts | 37 ++ src/adapters/chatgpt-web/model.ts | 22 +- src/chatgpt-web-compaction-policy.ts | 85 ++++ src/cli.ts | 27 +- src/compaction-model-config.ts | 67 +++ src/config.ts | 16 + src/dev-chat/cli.ts | 7 + src/server.ts | 39 +- src/types.ts | 4 + tests/cli.test.ts | 74 +++ tests/compaction-execution-policy.test.ts | 113 +++++ tests/compaction-identity.test.ts | 21 + tests/compaction-model-selection.test.ts | 475 ++++++++++++++++++ tests/fixtures/pro-model-picker.json | 23 + tests/launcher-helper-client.test.ts | 134 ++++- tests/retained-compaction.test.ts | 131 ++++- tests/runtime-layout.test.ts | 32 ++ tests/server-compaction.test.ts | 52 ++ 37 files changed, 1989 insertions(+), 41 deletions(-) create mode 100644 docs/compaction-model-selection.md create mode 100644 launcher/tests/compaction-model.test.cjs create mode 100644 src/chatgpt-web-compaction-policy.ts create mode 100644 src/compaction-model-config.ts create mode 100644 tests/compaction-execution-policy.test.ts create mode 100644 tests/compaction-identity.test.ts create mode 100644 tests/compaction-model-selection.test.ts create mode 100644 tests/fixtures/pro-model-picker.json diff --git a/README.ja.md b/README.ja.md index dde6af5ce..c0dc7200f 100644 --- a/README.ja.md +++ b/README.ja.md @@ -155,6 +155,11 @@ ChatGPT のツール呼び出しを現在の Codex タスクへ接続します ## 運用 +**設定 → Pro 圧縮モデル** では、自動 Pro タスクの要約だけに Extra High、GPT-5.6 Pro、 +GPT-5.5 Pro を選べます。通常のターンは元のモデルを使用し、既定値はメインタスクに従います。 +モデルの提供状況とアカウント上限は引き続き適用されます。 +詳しくは[圧縮モデルの選択](docs/compaction-model-selection.md)を参照してください。 + 安全なローカル診断には **アクティビティ**、エンドツーエンドのヘルスチェックには **設定 → 診断を実行**を使用します。設定から、保持中のブラウザーターンのキャンセルや、 アンインストール前の Codex 統合削除も行えます。すべてのブラウザーチェックポイントでスクリーンショットが必要な場合にのみ、 diff --git a/README.md b/README.md index 0fcb85d95..de577c6e7 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,10 @@ that option clicks **Allow once**, never a permanent grant. Use **Activity** for safe local diagnostics and **Settings → Run doctor** for end-to-end health. Settings can also cancel a retained browser turn or remove the Codex integration before uninstall. +**Settings → Pro compaction model** optionally uses Extra High, GPT-5.6 Pro, or GPT-5.5 Pro +for automatic Pro summaries while leaving ordinary task turns unchanged. The default follows the +main task. Model availability and account limits still apply; see +[compaction model selection](docs/compaction-model-selection.md). Set `CODEX_CHATGPT_WEB_BROWSER_DIAGNOSTICS=1` only when every browser checkpoint needs a screenshot. New installs use **Compatibility V1** for cross-backend subagents. **Native** preserves Codex's own diff --git a/README.zh-CN.md b/README.zh-CN.md index e53249526..290f43995 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -147,6 +147,10 @@ bun run app ## 日常操作 +**设置 → Pro 任务压缩模型** 可为自动 Pro 任务的摘要单独选择 Extra High、GPT-5.6 Pro 或 +GPT-5.5 Pro,普通任务回合保持原模型。默认跟随主任务,仍受模型可用性和账户额度限制;详见 +[压缩模型选择](docs/compaction-model-selection.md)。 + 使用 **活动** 页面查看安全的本地诊断,并通过 **设置 → 运行诊断** 执行端到端健康检查。设置页还可 取消保留的浏览器任务,或在卸载前移除 Codex 集成。仅在需要为每个浏览器检查点保存截图时设置 `CODEX_CHATGPT_WEB_BROWSER_DIAGNOSTICS=1`。 diff --git a/docs/architecture.md b/docs/architecture.md index 58f39ad9c..a6da0c55a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -155,6 +155,12 @@ every later tool action in the same turn continues to present the current turn c ChatGPT status rows become reasoning summaries, while stable prose between rows becomes native Codex commentary. +Automatic Pro compaction can optionally use a separately selected summary model. The bridge +keeps source ownership and transaction identity tied to the original request, and passes an +execution-only policy through the helper protocol. Explicit policies require live pre-send +effort/family verification and prohibit Pro staging. The default and handoff timeouts are unchanged; +see [compaction model selection](compaction-model-selection.md). + ## Installation and service lifecycle Each native desktop package contains Electron, a platform-matched pinned Bun executable, the diff --git a/docs/compaction-model-selection.md b/docs/compaction-model-selection.md new file mode 100644 index 000000000..ed61af53e --- /dev/null +++ b/docs/compaction-model-selection.md @@ -0,0 +1,71 @@ +# Optional model for Pro compaction + +**Settings → Pro compaction model** controls the summary step of an automatic +ChatGPT Web Pro task. It does not change the model used for ordinary task turns. + +| Choice | Summary execution | +| --- | --- | +| Follow main task (default) | Existing compaction behavior, with no override | +| GPT-5.6 Extra High | Explicit GPT-5.6 family at Extra High effort, not Pro | +| GPT-5.6 Pro | Explicit GPT-5.6 family at Pro effort | +| GPT-5.5 Pro | Explicit GPT-5.5 family at Pro effort | + +The setting is sampled when a new compaction starts. Saving a different choice +does not interrupt an active turn or compaction, restart the service, or change +Codex's model catalogue. Choosing the default removes the optional preference. +Zero Risk, Luna, non-Pro tasks, and accounts without the required capabilities +keep their existing behavior. + +## Why choose a different summary model? + +Extra High avoids a Pro submission for the summary. Choosing an older Pro family +still uses that family's Pro allowance; it is an alternative for users who want +Pro for the checkpoint without selecting the same family as their main task. +Availability, shared allowances and reset schedules depend on the account and +ChatGPT's current rules. This is explicit model selection, not a quota bypass. +See the [official model and usage-limit documentation](https://help.openai.com/en/articles/20001354-gpt-56-in-chatgpt). + +Reducing summary latency and avoiding some Pro-specific failure modes are goals, +not guaranteed outcomes. Changing the summary model can change checkpoint +quality. It does not repair browser disconnects, authentication failures, invalid +handoffs, or an unavailable model. + +## Execution boundaries + +1. Codex decides when to compact and sends its canonical history and native turn + identity to the bridge. This setting does not change context thresholds. +2. The original request still identifies the retained source, shared compaction + transaction, cancellation and retirement. Only the summary execution gets the + selected model/effort override. +3. The existing retained-source handoff is preserved. An automatic task without + a usable retained source uses the existing read-only fresh-chat fallback. + Browser-only summaries use the same execution preference. Summary generation + does not gain ordinary Codex work tools. +4. For an explicit override, multipart staging is restricted to non-Pro levels. + An indivisible record that requires Pro's larger message envelope fails before + sending; the bridge does not silently spend a Pro request on staging. +5. Before submission, the live model controls must prove the requested effort + and family. A missing family, changed + control or unavailable effort causes an explicit error, not a fallback. +6. The existing checkpoint validation and browser-release checks still run. + Codex receives its replacement context and subsequent work uses the main + task's original model. + +The five-minute structured handoff liveness budget is unchanged. Existing +multipart progress acknowledgements can renew the relevant phase budget; this +is not a new fifty-minute timeout or an automatic retry of an accepted prompt. + +## Configuration and compatibility + +The optional core configuration field is `compactionModel`, with values +`extra-high`, `5.6-pro`, or `5.5-pro`. An omitted field means follow-main. +Use the launcher setting to persist it through the authenticated configuration +command. The override is not part of the retained source's execution namespace. + +Both runtime and browser helper must support the `compaction-execution` feature. +An older helper cannot silently ignore an explicit choice. Unknown settings and +inconsistent execution payloads are rejected. + +The production service reads this preference for each new eligible compaction. +The isolated DEV named-chat CLI snapshots its configuration at startup: reopen +that CLI after changing the DEV preference. The launcher itself need not restart. diff --git a/launcher/electron/main.cjs b/launcher/electron/main.cjs index 492b95087..b2c3813bf 100644 --- a/launcher/electron/main.cjs +++ b/launcher/electron/main.cjs @@ -408,6 +408,13 @@ function validateBrowserInteractionMode(value) { return value; } +function validateCompactionModel(value) { + if (value !== null && value !== "extra-high" && value !== "5.6-pro" && value !== "5.5-pro") { + throw new Error("Compaction model must be follow, extra-high, 5.6-pro, or 5.5-pro"); + } + return value; +} + function validateBounds(value) { if (!value || typeof value !== "object") throw new Error("Browser bounds are required"); for (const key of ["x", "y", "width", "height"]) { @@ -430,6 +437,7 @@ function registerIpc({ logger, stateStore }) { userData: launcherUserData, }, state: stateStore.read(), + compactionModel: runtimeHost.compactionModel(), browser: browserHost?.snapshot() ?? null, connectorName: runtimeHost.browserConnectorName(), connectorNames: { @@ -819,6 +827,10 @@ function registerIpc({ logger, stateStore }) { if (!IS_DEV_PROFILE && result.configured) startCatalogVerificationMonitor({ logger, stateStore }); return { state, credentialsRequired: false, targetMode: mode }; }); + handle("launcher:compaction-model", async (_event, rawModel) => { + // Config-only: active turns and an already-started compaction keep their pinned model. + return runtimeHost.setCompactionModel(validateCompactionModel(rawModel)); + }); handle("launcher:set-preference", (_event, key, value) => { const ordinary = key === "keepRunningOnClose" || key === "showBrowserDuringTurns"; if (!ordinary) throw new Error("Unknown preference"); diff --git a/launcher/electron/preload.cjs b/launcher/electron/preload.cjs index b474b285e..d51802e8c 100644 --- a/launcher/electron/preload.cjs +++ b/launcher/electron/preload.cjs @@ -42,6 +42,7 @@ contextBridge.exposeInMainWorld("codexWebLauncher", { setAutostart: (enabled) => ipcRenderer.invoke("launcher:autostart", enabled), setBiggerContext: (enabled) => ipcRenderer.invoke("launcher:bigger-context", enabled), setZeroRiskPro: (enabled) => ipcRenderer.invoke("launcher:zero-risk-pro", enabled), + setCompactionModel: (model) => ipcRenderer.invoke("launcher:compaction-model", model), setBrowserInteractionMode: (mode) => ipcRenderer.invoke("launcher:browser-interaction-mode", mode), setPreference: (key, value) => ipcRenderer.invoke("launcher:set-preference", key, value), setSidebarState: (state) => ipcRenderer.invoke("launcher:sidebar-state", state), diff --git a/launcher/electron/runtime-supervisor.cjs b/launcher/electron/runtime-supervisor.cjs index a05443f49..dd9dff21b 100644 --- a/launcher/electron/runtime-supervisor.cjs +++ b/launcher/electron/runtime-supervisor.cjs @@ -255,6 +255,12 @@ function validateConfig(config, descriptorPath, platform = process.platform, lau && typeof config.experimentalBiggerContext !== "boolean") { throw new Error("Runtime configuration has an invalid experimentalBiggerContext"); } + if (config.compactionModel !== undefined + && config.compactionModel !== "extra-high" + && config.compactionModel !== "5.6-pro" + && config.compactionModel !== "5.5-pro") { + throw new Error("Runtime configuration has an invalid compactionModel"); + } if (config.stallTimeoutSec !== undefined && (!Number.isFinite(config.stallTimeoutSec) || config.stallTimeoutSec <= 0)) { throw new Error("Runtime configuration has an invalid stallTimeoutSec"); diff --git a/launcher/electron/runtime.cjs b/launcher/electron/runtime.cjs index 49fdcf044..0d55a153a 100644 --- a/launcher/electron/runtime.cjs +++ b/launcher/electron/runtime.cjs @@ -1096,6 +1096,43 @@ class RuntimeHost { return { ...result, mode, enabled: enabled === true }; } + compactionModel() { + return this.runtimeConfigSnapshot().config?.compactionModel ?? null; + } + + async setCompactionModel(value) { + if (value !== null && value !== "extra-high" && value !== "5.6-pro" && value !== "5.5-pro") { + throw new Error("Compaction model must be follow, extra-high, 5.6-pro, or 5.5-pro"); + } + const current = this.runtimeConfigSnapshot(); + if (!current.configured) { + throw new Error("Install the Codex integration before changing the compaction model"); + } + if ((current.config?.compactionModel ?? null) === value) return { compactionModel: value }; + const args = [ + ...(this.launcherProfile === "development" ? ["dev"] : []), + "config", + "compaction-model", + value ?? "follow", + "--launcher-control", + ]; + await this.run("compaction-model", args, { + ...(this.launcherProfile === "development" ? { + embedded: true, + environment: this.devSetupEnvironment(), + } : {}), + env: this.launcherControlEnvironment(), + message: "Saving the Pro compaction model", + successMessage: "Pro compaction model saved for the next compaction", + timeoutMs: CORE_SETUP_TIMEOUT_MS, + }); + const saved = this.compactionModel(); + if (saved !== value) { + throw new Error("Runtime configuration did not persist the requested compaction model"); + } + return { compactionModel: saved }; + } + async setZeroRiskPro(enabled) { const current = this.runtimeConfigSnapshot(); if (!current.configured) { diff --git a/launcher/src/App.tsx b/launcher/src/App.tsx index bee8f1c01..f94983591 100644 --- a/launcher/src/App.tsx +++ b/launcher/src/App.tsx @@ -14,6 +14,7 @@ import { Icon, type IconName } from "./icons"; import type { BrowserInteractionMode, BrowserState, + CompactionModel, DoctorReport, Language, LauncherSnapshot, @@ -97,6 +98,10 @@ export function App() { : current); }, []); + const updateCompactionModel = useCallback((compactionModel: CompactionModel) => { + setSnapshot((current) => current ? { ...current, compactionModel } : current); + }, []); + if (!api) return ; if (!snapshot) return ; @@ -130,6 +135,7 @@ export function App() { operation={operation} setError={setError} snapshot={snapshot} + updateCompactionModel={updateCompactionModel} updateState={updateState} /> )} @@ -330,6 +336,7 @@ function LauncherShell({ operation, setError, snapshot, + updateCompactionModel, updateState, }: { browser: BrowserState | null; @@ -339,6 +346,7 @@ function LauncherShell({ operation: OperationState | null; setError: (error: string | null) => void; snapshot: LauncherSnapshot; + updateCompactionModel: (value: CompactionModel) => void; updateState: (state: LauncherState) => void; }) { const interactionSetupComplete = snapshot.state.coreSetupComplete === true @@ -706,6 +714,7 @@ function LauncherShell({ language={language} setError={setError} snapshot={snapshot} + updateCompactionModel={updateCompactionModel} updateState={updateState} /> ) : null} @@ -1582,6 +1591,7 @@ function SettingsSurface({ language, setError, snapshot, + updateCompactionModel, updateState, }: { configureInteractionMode: (mode: BrowserInteractionMode) => void; @@ -1590,6 +1600,7 @@ function SettingsSurface({ language: Language; setError: (error: string | null) => void; snapshot: LauncherSnapshot; + updateCompactionModel: (value: CompactionModel) => void; updateState: (state: LauncherState) => void; }) { const [doctor, setDoctor] = useState(null); @@ -1650,6 +1661,18 @@ function SettingsSurface({ setBusy(false); } }; + const setCompactionModel = async (value: CompactionModel) => { + setBusy(true); + setError(null); + try { + const result = await api!.setCompactionModel(value); + updateCompactionModel(result.compactionModel); + } catch (cause) { + setError(messageOf(cause)); + } finally { + setBusy(false); + } + }; const uninstallIntegration = async () => { setBusy(true); setError(null); @@ -1684,6 +1707,14 @@ function SettingsSurface({ mode={snapshot.state.browserInteractionMode} onChange={(mode) => void setInteractionMode(mode)} /> + + void setCompactionModel(value)} + value={snapshot.compactionModel} + /> + void; + value: CompactionModel; +}) { + return ( + + ); +} + function StateDot({ state }: { state: "idle" | "ready" | "busy" | "error" }) { return