feat(codex): add unified session history toggle for official providers

Codex buckets resume history by the model_provider id recorded in each
session: official runs (no key, built-in "openai") and cc-switch
third-party runs (shared "custom") are mutually invisible in the resume
picker. Add an opt-in setting that runs official providers under the
shared "custom" id so future official sessions land in the same history
bucket as third-party ones. Forward-only by design: existing sessions
are not migrated.

When enabled, official live config.toml gets model_provider = "custom"
plus a [model_providers.custom] entry that mirrors the built-in openai
provider (requires_openai_auth routes auth to the ChatGPT login in
auth.json, name "OpenAI" keeps is_openai() feature gates, explicit
supports_websockets/wire_api restore built-in defaults). auth.json is
untouched.

Key invariants:
- Injection lives only in the live config: switch-away backfill strips
  the exact injected shape, so stored provider configs stay clean and
  turning the toggle off fully reverts on the next write.
- Toggle changes apply immediately via a takeover-aware reapply: when
  the proxy owns the live config (backup/placeholder present), only the
  live backup is updated, mirroring the provider-switch path.
- The takeover backup path runs the same injection so a takeover
  release restores a config that still carries the unified routing.
- Injection refuses to activate a foreign [model_providers.custom]
  table (e.g. stale entry with a third-party base_url) to avoid routing
  ChatGPT OAuth traffic to an unknown backend.

The toggle lives under Settings → Codex App Enhancements; the
description warns that resuming old sessions across providers may fail
because encrypted_content reasoning only decrypts on the backend that
created it (upstream treats cross-provider resume as unsupported).
This commit is contained in:
Jason
2026-06-12 10:40:51 +08:00
parent 4f355970e1
commit 948d762792
14 changed files with 406 additions and 2 deletions
+11 -1
View File
@@ -1,5 +1,5 @@
import { useTranslation } from "react-i18next";
import { KeyRound } from "lucide-react";
import { History, KeyRound } from "lucide-react";
import type { SettingsFormState } from "@/hooks/useSettings";
import { ToggleRow } from "@/components/ui/toggle-row";
@@ -30,6 +30,16 @@ export function CodexAuthSettings({
onChange({ preserveCodexOfficialAuthOnSwitch: value })
}
/>
<ToggleRow
icon={<History className="h-4 w-4 text-sky-500" />}
title={t("settings.unifyCodexSessionHistory")}
description={t("settings.unifyCodexSessionHistoryDescription")}
checked={settings.unifyCodexSessionHistory ?? false}
onCheckedChange={(value) =>
onChange({ unifyCodexSessionHistory: value })
}
/>
</section>
);
}
+3
View File
@@ -118,6 +118,7 @@ export function useSettingsForm(): UseSettingsFormResult {
skipClaudeOnboarding: data.skipClaudeOnboarding ?? false,
preserveCodexOfficialAuthOnSwitch:
data.preserveCodexOfficialAuthOnSwitch ?? false,
unifyCodexSessionHistory: data.unifyCodexSessionHistory ?? false,
claudeConfigDir: sanitizeDir(data.claudeConfigDir),
codexConfigDir: sanitizeDir(data.codexConfigDir),
geminiConfigDir: sanitizeDir(data.geminiConfigDir),
@@ -143,6 +144,7 @@ export function useSettingsForm(): UseSettingsFormResult {
enableClaudePluginIntegration: false,
skipClaudeOnboarding: false,
preserveCodexOfficialAuthOnSwitch: false,
unifyCodexSessionHistory: false,
language: readPersistedLanguage(),
} as SettingsFormState);
@@ -182,6 +184,7 @@ export function useSettingsForm(): UseSettingsFormResult {
skipClaudeOnboarding: serverData.skipClaudeOnboarding ?? false,
preserveCodexOfficialAuthOnSwitch:
serverData.preserveCodexOfficialAuthOnSwitch ?? false,
unifyCodexSessionHistory: serverData.unifyCodexSessionHistory ?? false,
claudeConfigDir: sanitizeDir(serverData.claudeConfigDir),
codexConfigDir: sanitizeDir(serverData.codexConfigDir),
geminiConfigDir: sanitizeDir(serverData.geminiConfigDir),
+2
View File
@@ -670,6 +670,8 @@
"codexAuth": "Codex App Enhancements",
"preserveCodexOfficialAuthOnSwitch": "Keep official login when switching third-party providers",
"preserveCodexOfficialAuthOnSwitchDescription": "When enabled, you can use the Codex app's official plugins, mobile remote control, and other features while using third-party APIs.",
"unifyCodexSessionHistory": "Unified Codex session history",
"unifyCodexSessionHistoryDescription": "When enabled, the official subscription runs under the shared \"custom\" provider id, so future official sessions appear in the same history list as third-party sessions (existing sessions are not migrated). Note: resuming an old session across providers may fail because its encrypted_content reasoning can only be decrypted by the backend that created it.",
"appVisibility": {
"title": "Homepage Display",
"description": "Choose which apps to show on the homepage",
+2
View File
@@ -670,6 +670,8 @@
"codexAuth": "Codex アプリ拡張",
"preserveCodexOfficialAuthOnSwitch": "サードパーティ切替時に公式ログインを保持",
"preserveCodexOfficialAuthOnSwitchDescription": "オンにすると、サードパーティ API を使用する際に Codex アプリの公式プラグインやスマホからのリモート操作などの機能を利用できます。",
"unifyCodexSessionHistory": "Codex セッション履歴を統一",
"unifyCodexSessionHistoryDescription": "オンにすると、公式サブスクリプションも共有の custom プロバイダー ID で動作し、今後の公式セッションはサードパーティのセッションと同じ履歴リストに表示されます(既存セッションは移行しません)。注意:プロバイダーをまたいで古いセッションを再開すると、encrypted_content の推論内容を相手のバックエンドが復号できず、再開に失敗する場合があります。",
"appVisibility": {
"title": "ホームページ表示",
"description": "ホームページに表示するアプリを選択",
+2
View File
@@ -670,6 +670,8 @@
"codexAuth": "Codex 應用增強",
"preserveCodexOfficialAuthOnSwitch": "切換第三方時保留官方登入",
"preserveCodexOfficialAuthOnSwitchDescription": "開啟後可以在使用第三方 API 的時候使用 Codex 應用的官方外掛、手機遠端操作等功能",
"unifyCodexSessionHistory": "統一 Codex 會話歷史",
"unifyCodexSessionHistoryDescription": "開啟後,官方訂閱將以共享的 custom 供應商標識執行,未來的官方會話與第三方會話出現在同一歷史清單中(不遷移既有會話)。注意:跨供應商繼續舊會話時,對方後端可能無法解密會話中的 encrypted_content 推理內容,導致繼續失敗",
"appVisibility": {
"title": "主頁面顯示",
"description": "選擇在主頁面顯示的應用程式",
+2
View File
@@ -670,6 +670,8 @@
"codexAuth": "Codex 应用增强",
"preserveCodexOfficialAuthOnSwitch": "切换第三方时保留官方登录",
"preserveCodexOfficialAuthOnSwitchDescription": "开启后可以在使用第三方 API 的时候使用 Codex 应用的官方插件、手机远程操作等功能",
"unifyCodexSessionHistory": "统一 Codex 会话历史",
"unifyCodexSessionHistoryDescription": "开启后,官方订阅将以共享的 custom 供应商标识运行,未来的官方会话与第三方会话出现在同一历史列表中(不迁移既有会话)。注意:跨供应商继续旧会话时,对方后端可能无法解密会话中的 encrypted_content 推理内容,导致继续失败",
"appVisibility": {
"title": "主页面显示",
"description": "选择在主页面显示的应用",
+1
View File
@@ -16,6 +16,7 @@ export const settingsSchema = z.object({
launchOnStartup: z.boolean().optional(),
enableLocalProxy: z.boolean().optional(),
preserveCodexOfficialAuthOnSwitch: z.boolean().optional(),
unifyCodexSessionHistory: z.boolean().optional(),
language: z.enum(["en", "zh", "zh-TW", "ja"]).optional(),
// 设备级目录覆盖
+3
View File
@@ -351,6 +351,9 @@ export interface Settings {
enableFailoverToggle?: boolean;
// Preserve Codex ChatGPT login in auth.json when switching third-party providers
preserveCodexOfficialAuthOnSwitch?: boolean;
// Run official Codex under the shared "custom" provider id so future
// sessions share one resume-history bucket with third-party providers
unifyCodexSessionHistory?: boolean;
// User has confirmed the failover toggle first-run notice
failoverConfirmed?: boolean;
// User has confirmed the first-run welcome notice