diff --git a/docs-site/src/content/docs/fr/guides/codex-integration.md b/docs-site/src/content/docs/fr/guides/codex-integration.md index 9a603b910a..8fff31edaf 100644 --- a/docs-site/src/content/docs/fr/guides/codex-integration.md +++ b/docs-site/src/content/docs/fr/guides/codex-integration.md @@ -421,4 +421,6 @@ Codex. Seule l'exécution explicite de `ocx stop` ou `ocx service stop` restaure Une transition de fournisseur peut renvoyer `history_paginated_requires_native_writer` si le stockage concerné prend en charge la pagination, même pour ses lignes legacy. Cette raison ne refuse plus la configuration Codex, le profil de référence ni le catalogue de modèles. `ocx sync` et `ocx start` écrivent toujours ces fichiers et définissent `model_catalog_json`, afin que le sélecteur de modèles Codex continue d’afficher tous les modèles routés par OpenCodex. Seule cette raison interrompt le réétiquetage de l’historique des conversations, car Codex attribue les numéros d’historique paginé dans son propre processus d’écriture et aucune nouvelle tentative n’y change rien. Toute autre raison de contrôle préalable de l’historique — une base d’état illisible, un historique dont l’identité a changé, ou un contrôle préalable qui n’a pas pu s’exécuter — refuse encore toute la transition et l’annule, car ces cas peuvent réussir plus tard. Dans cet état, OpenCodex ne modifie jamais les fichiers d’historique paginé ni les lignes de conversation. Les conversations existantes conservent le fournisseur déjà associé et ne sont pas migrées ; les nouvelles conversations passent par le proxy. Lorsque le réétiquetage est interrompu, une table `[model_providers.opencodex]` déjà présente dans le répertoire d’accueil est conservée plutôt que retirée, y compris sous la forme root-override (loopback), afin que les conversations dont les lignes sont étiquetées `opencodex` gardent un identifiant de fournisseur qui existe encore. Le CLI affiche `Codex resume history: left to Codex's native writer (history_paginated_requires_native_writer)`. `ocx restore` et la suppression de la configuration Codex refusent toujours sur `history_paginated_requires_native_writer`. Retirer la définition `[model_providers.opencodex]` alors que des lignes de conversation la référencent encore rendrait ces conversations irrésolubles, et le chemin de restauration n’a aucun moyen de conserver une table de fournisseur de compatibilité. Un répertoire d’accueil déjà paginé ne peut pas actuellement être désinstallé par le produit ; c’est un travail ouvert connu, et non le comportement voulu. +Lors du retour au mode de remplacement de l’URL racine, OpenCodex conserve la définition `[model_providers.opencodex]` existante avant de valider la configuration, même si la vérification préalable de l’historique réussit. Les anciennes conversations `opencodex` peuvent ainsi toujours retrouver leur fournisseur si Codex migre l’historique après cette validation ou pendant le démarrage du traitement en arrière-plan. Les nouvelles conversations utilisent le fournisseur racine sélectionné ; la restauration explicite conserve ses contrôles de suppression distincts. + Ne réécrivez pas un historique paginé actif ni une ligne de conversation pour forcer une migration. Fermez la conversation avant toute récupération et signalez l’erreur exacte et les versions sans publier de données privées. Une sauvegarde ou le succès d’un script ne prouve pas le rétablissement de l’affichage : vérifiez la conversation après réouverture de Codex. diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index 877117babe..386775f561 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -875,6 +875,8 @@ When a routed preferred model may receive V2 work from a native ChatGPT parent, When an affected history store supports paginated records, a provider transition may return `history_paginated_requires_native_writer`. That reason no longer refuses the Codex configuration, the reference profile, or the model catalog. `ocx sync` and `ocx start` still write those files and set `model_catalog_json`, so the Codex model picker keeps showing every OpenCodex-routed model. Only this one reason stands the conversation-history relabel down, because Codex allocates paginated rollout ordinals in its own writer and no retry changes that. Any other history preflight reason — an unreadable state database, a rollout whose identity changed, or a preflight that could not run — still refuses the whole transition and rolls it back, because those may succeed on a later attempt. OpenCodex never modifies paginated rollout files or thread rows in this state. Existing conversations keep whatever provider they are already tagged with and are not migrated; new conversations route through the proxy normally. When the relabel stands down, a `[model_providers.opencodex]` table that the home already had is kept rather than retired, even in the root-override (loopback) form, so conversations whose rows are tagged `opencodex` keep a provider id that still exists. This includes legacy rows in a migration-capable store. The CLI prints `Codex resume history: left to Codex's native writer (history_paginated_requires_native_writer)`. +When returning to the root-override form, OpenCodex retains an existing `[model_providers.opencodex]` definition before committing the configuration, even if history preflight currently passes. This keeps older `opencodex` conversations resolvable if Codex migrates history after that commit or while the background worker starts. New conversations still use the selected root provider; explicit restore keeps its separate removal guards. + `ocx restore` and Codex config removal still refuse on `history_paginated_requires_native_writer`. Stripping the `[model_providers.opencodex]` definition while thread rows still reference it would make those conversations unresolvable, and the restore path has no way to keep a compatibility provider table. A home that is already paginated cannot currently be uninstalled through the product; that is known open work rather than intended behaviour. Do not rewrite an active paginated rollout or thread row to migrate those conversations yourself. Close the affected conversation before any recovery, and report the exact error and versions without uploading private history. A backup or a successful script alone does not prove the conversation is visible again. Check the restored conversation in Codex after reopening. diff --git a/docs-site/src/content/docs/ja/guides/codex-integration.md b/docs-site/src/content/docs/ja/guides/codex-integration.md index cc44ec28c3..4f6afdb37e 100644 --- a/docs-site/src/content/docs/ja/guides/codex-integration.md +++ b/docs-site/src/content/docs/ja/guides/codex-integration.md @@ -283,4 +283,6 @@ opencodex が管理対象 [バックグラウンドサービス](/reference/cli/ 対象の履歴ストアがページ分割をサポートする場合、プロバイダー変更は `history_paginated_requires_native_writer` を返すことがあります。この理由では、Codex の設定、参照プロファイル、モデルカタログは拒否されません。`ocx sync` と `ocx start` はこれらのファイルを書き込み、`model_catalog_json` を設定するため、Codex のモデル選択には OpenCodex 経由のモデルがすべて表示され続けます。会話履歴の再ラベル付けを控えるのはこの理由だけの場合です。ページ分割された履歴の番号は Codex 自身の書き込み処理が割り当て、再試行しても変わりません。読み取れない状態データベース、識別子が変わった履歴、実行できなかった事前検査など、それ以外の履歴事前検査の理由では、後から成功する可能性があるため、遷移全体を拒否してロールバックします。この状態では OpenCodex はページ分割された履歴ファイルやスレッド行を変更しません。既存の会話はすでに付いているプロバイダーのまま移行されず、新しい会話は通常どおりプロキシ経由でルーティングされます。再ラベル付けを控えるとき、ホームに既にある `[model_providers.opencodex]` テーブルは廃止せず残します。ルート上書き(loopback)形式でも同じで、行が `opencodex` と付いている会話は、まだ存在するプロバイダー id を保てます。移行可能なストアの legacy 行も対象です。CLI は `Codex resume history: left to Codex's native writer (history_paginated_requires_native_writer)` と表示します。`ocx restore` と Codex 設定の削除は、いまも `history_paginated_requires_native_writer` で拒否されます。スレッド行がまだ参照しているのに `[model_providers.opencodex]` 定義を外すと、それらの会話は解決できなくなり、復元経路には互換プロバイダー表を残す手段がありません。すでにページ分割されているホームは、現状では製品からアンインストールできません。意図した動作ではなく、既知の未解決作業です。 +ルート URL 上書き方式に戻すとき、履歴の事前確認が成功していても、OpenCodex は設定を確定する前に既存の `[model_providers.opencodex]` 定義を保持します。確定後やバックグラウンドの履歴処理開始中に Codex が履歴形式を移行しても、以前の `opencodex` 会話はプロバイダーを引き続き解決できます。新しい会話は選択されたルートプロバイダーを使い、明示的な復元には従来の個別の削除チェックが適用されます。 + 会話を移行しようとして使用中のページ分割履歴やスレッド行を書き換えないでください。復元前に対象の会話を閉じ、個人の履歴を公開せず正確なエラーとバージョンを報告してください。バックアップやスクリプトの成功だけでは表示の復元は証明されません。再度開いた Codex で確認してください。 diff --git a/docs-site/src/content/docs/ko/guides/codex-integration.md b/docs-site/src/content/docs/ko/guides/codex-integration.md index 0d049a01fc..0d92b524e5 100644 --- a/docs-site/src/content/docs/ko/guides/codex-integration.md +++ b/docs-site/src/content/docs/ko/guides/codex-integration.md @@ -387,4 +387,6 @@ opencodex가 managed [background service](/reference/cli/#ocx-service)로 실행 영향받는 기록 저장소가 페이지 분할을 지원하면 프로바이더 전환이 `history_paginated_requires_native_writer`를 반환할 수 있습니다. 이 이유로는 Codex 설정, 참조 프로필, 모델 카탈로그를 더 이상 거부하지 않습니다. `ocx sync`와 `ocx start`는 해당 파일과 `model_catalog_json`을 계속 쓰므로 Codex 모델 선택기에는 OpenCodex가 라우팅하는 모델이 모두 그대로 보입니다. 대화 기록의 프로바이더 재지정을 건너뛰는 것은 이 이유뿐이며, 페이지 분할 순번은 Codex 자체의 네이티브 기록 작성자가 할당하고 재시도해도 달라지지 않기 때문입니다. 읽을 수 없는 상태 데이터베이스, 식별자가 바뀐 대화 원본, 실행하지 못한 사전 검사처럼 다른 기록 사전 검사 이유는 나중에 성공할 수 있으므로 전환 전체를 거부하고 되돌립니다. 이 상태에서 OpenCodex는 페이지 분할 대화 원본이나 스레드 행을 수정하지 않습니다. 기존 대화는 이미 붙어 있는 프로바이더를 유지하고 이전되지 않으며, 새 대화는 평소처럼 프록시를 통해 라우팅됩니다. 재지정을 건너뛸 때 홈에 이미 있던 `[model_providers.opencodex]` 테이블은 폐기하지 않고 유지합니다. root-override(loopback) 형식에서도 같아서, 행이 `opencodex`로 표시된 대화는 아직 존재하는 프로바이더 id를 유지합니다. 변환 가능한 저장소의 `legacy` 행도 포함됩니다. CLI는 `Codex resume history: left to Codex's native writer (history_paginated_requires_native_writer)`를 출력합니다. `ocx restore`와 Codex 설정 제거는 여전히 `history_paginated_requires_native_writer`로 거부됩니다. 스레드 행이 아직 참조하는데 `[model_providers.opencodex]` 정의를 걷어내면 그 대화를 해석할 수 없고, 복원 경로에는 호환 프로바이더 테이블을 남겨 둘 방법이 없습니다. 이미 페이지 분할된 홈은 지금은 제품으로 제거할 수 없습니다. 의도한 동작이 아니라 알려진 미해결 작업입니다. +루트 URL 재정의 방식으로 돌아갈 때 OpenCodex는 기록 사전 점검이 통과하더라도 기존 `[model_providers.opencodex]` 정의를 설정 적용 전에 유지합니다. 설정 적용 후나 백그라운드 기록 작업 시작 중에 Codex가 기록 형식을 전환해도 이전 `opencodex` 대화가 제공자를 계속 찾을 수 있습니다. 새 대화는 선택된 루트 제공자를 사용하며, 명시적 복원에는 기존의 별도 제거 검사가 적용됩니다. + 대화를 강제로 이전하려고 실행 중인 페이지 분할 대화 원본이나 스레드 행을 고치지 마세요. 복구 전에 해당 대화를 닫은 뒤, 개인 대화 내용을 올리지 말고 정확한 오류와 버전을 보고하세요. 백업이나 스크립트 성공만으로 표시 복구가 증명되지는 않으므로 Codex를 다시 열어 확인하세요. diff --git a/docs-site/src/content/docs/ru/guides/codex-integration.md b/docs-site/src/content/docs/ru/guides/codex-integration.md index 1cca5f59fd..1394091fe9 100644 --- a/docs-site/src/content/docs/ru/guides/codex-integration.md +++ b/docs-site/src/content/docs/ru/guides/codex-integration.md @@ -415,4 +415,6 @@ ocx restore back # point plain Codex at the running proxy again Если затронутое хранилище поддерживает постраничную историю, смена провайдера может вернуть `history_paginated_requires_native_writer`, в том числе для строк legacy. По этой причине больше не отклоняются конфигурация Codex, опорный профиль и каталог моделей. `ocx sync` и `ocx start` по-прежнему записывают эти файлы и задают `model_catalog_json`, поэтому выбор модели Codex продолжает показывать все модели, маршрутизируемые через OpenCodex. Переразметку истории разговоров останавливает только эта причина: порядковые номера постраничной истории выделяет собственный процесс записи Codex, и повторная попытка этого не меняет. Любая другая причина предварительной проверки истории — нечитаемая база состояния, история со сменившейся идентификацией или проверка, которую не удалось запустить, — по-прежнему отклоняет весь переход и откатывает его, потому что такие случаи могут пройти позже. В этом состоянии OpenCodex не изменяет постраничные файлы истории и строки тредов. Существующие разговоры сохраняют уже назначенного провайдера и не мигрируют; новые разговоры идут через прокси как обычно. Когда переразметка останавливается, таблица `[model_providers.opencodex]`, уже бывшая в домашнем каталоге, сохраняется, а не снимается, в том числе в форме root-override (loopback), чтобы разговоры со строками, помеченными `opencodex`, сохраняли существующий идентификатор провайдера. CLI выводит `Codex resume history: left to Codex's native writer (history_paginated_requires_native_writer)`. `ocx restore` и удаление конфигурации Codex по-прежнему отказывают по `history_paginated_requires_native_writer`. Удаление определения `[model_providers.opencodex]`, пока строки тредов на него ссылаются, сделало бы эти разговоры неразрешимыми, а путь восстановления не умеет оставлять таблицу совместимости провайдера. Домашний каталог, уже переведённый на постраничную историю, сейчас нельзя удалить средствами продукта; это известная открытая задача, а не задуманное поведение. +При возврате к режиму переопределения корневого URL OpenCodex сохраняет существующее определение `[model_providers.opencodex]` до фиксации конфигурации, даже если предварительная проверка истории успешна. Поэтому старые разговоры `opencodex` сохраняют доступ к своему провайдеру, если Codex преобразует историю после фиксации или во время запуска фоновой обработки. Новые разговоры используют выбранный корневой провайдер; явное восстановление по-прежнему выполняет отдельные проверки удаления. + Не переписывайте активную постраничную историю или строку треда, чтобы самостоятельно перенести разговоры. Закройте разговор перед восстановлением и сообщите точную ошибку и версии без публикации личной истории. Наличие резервной копии или успешный скрипт не доказывает восстановление отображения: проверьте разговор после повторного открытия Codex. diff --git a/docs-site/src/content/docs/tr/guides/codex-integration.md b/docs-site/src/content/docs/tr/guides/codex-integration.md index 436c995bdb..17a8630c21 100644 --- a/docs-site/src/content/docs/tr/guides/codex-integration.md +++ b/docs-site/src/content/docs/tr/guides/codex-integration.md @@ -472,4 +472,6 @@ service stop` yerel Codex'i geri yükler. Etkilenen geçmiş deposu sayfalamayı destekliyorsa sağlayıcı değişimi `history_paginated_requires_native_writer` döndürebilir; legacy satırlar da buna dahildir. Bu neden artık Codex yapılandırmasını, başvuru profilini veya model kataloğunu reddetmez. `ocx sync` ve `ocx start` bu dosyaları yazmaya ve `model_catalog_json` yolunu ayarlamaya devam eder; böylece Codex model seçicisi OpenCodex üzerinden yönlendirilen her modeli göstermeyi sürdürür. Konuşma geçmişinin yeniden etiketlenmesini durduran yalnızca bu nedendir, çünkü sayfalanmış geçmiş sıra numaralarını Codex’in kendi yerel yazıcısı atar ve yeniden denemek bunu değiştirmez. Okunamayan bir durum veritabanı, kimliği değişmiş bir geçmiş veya çalıştırılamayan bir ön kontrol gibi diğer geçmiş ön kontrol nedenleri, daha sonra başarılı olabilecekleri için hâlâ tüm değişimi reddeder ve geri alır. Bu durumda OpenCodex sayfalanmış geçmiş dosyalarını veya iş parçacığı satırlarını değiştirmez. Mevcut konuşmalar zaten etiketlendikleri sağlayıcıda kalır ve taşınmaz; yeni konuşmalar proxy üzerinden normal şekilde yönlendirilir. Yeniden etiketleme durduğunda, ev dizininde zaten bulunan bir `[model_providers.opencodex]` tablosu kaldırılmaz, kök-override (loopback) biçimde bile tutulur; böylece satırları `opencodex` olarak etiketlenmiş konuşmalar hâlâ var olan bir sağlayıcı kimliğini korur. CLI şunu yazdırır: `Codex resume history: left to Codex's native writer (history_paginated_requires_native_writer)`. `ocx restore` ve Codex yapılandırmasının kaldırılması `history_paginated_requires_native_writer` nedeniyle hâlâ reddedilir. İş parçacığı satırları hâlâ ona başvuruyken `[model_providers.opencodex]` tanımını kaldırmak o konuşmaları çözülemez yapar ve geri yükleme yolu uyumluluk sağlayıcı tablosunu tutamaz. Zaten sayfalanmış bir ev dizini şu anda ürün üzerinden kaldırılamaz; bu amaçlanan davranış değil, bilinen açık iştir. +Kök URL geçersiz kılma biçimine dönülürken OpenCodex, geçmiş ön kontrolü başarılı olsa bile yapılandırmayı kaydetmeden önce mevcut `[model_providers.opencodex]` tanımını korur. Böylece Codex, kayıttan sonra veya arka plan geçmiş işlemi başlarken geçmiş biçimini değiştirirse eski `opencodex` konuşmaları sağlayıcılarını bulmaya devam eder. Yeni konuşmalar seçili kök sağlayıcıyı kullanır; açıkça istenen geri yükleme, mevcut ayrı kaldırma kontrollerini korur. + Konuşmaları kendiniz taşımak için etkin sayfalanmış geçmişi veya iş parçacığı satırını yeniden yazmayın. Kurtarmadan önce konuşmayı kapatın ve özel geçmişi yayımlamadan tam hatayı ve sürümleri bildirin. Yedek veya başarılı betik görüntünün düzeldiğini kanıtlamaz; Codex’i yeniden açıp konuşmayı kontrol edin. diff --git a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md index dfdac6d98a..13612608e9 100644 --- a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md +++ b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md @@ -358,4 +358,6 @@ ocx restore back # point plain Codex at the running proxy again 如果受影响的历史存储支持分页,提供商切换可能返回 `history_paginated_requires_native_writer`。该原因不再拒绝写入 Codex 配置、参考配置档和模型目录。`ocx sync` 与 `ocx start` 仍会写入这些文件并设置 `model_catalog_json`,因此 Codex 模型选择器会继续显示所有经 OpenCodex 路由的模型。只有这一条原因会让会话历史的重新标记停手,因为分页历史序号由 Codex 自己的写入器分配,重试也不会改变。无法读取的状态数据库、身份已变的历史文件、未能运行的预检等其他历史预检原因仍会拒绝整个切换并回滚,因为那些情况以后可能成功。在此状态下,OpenCodex 不会修改分页历史文件或线程行。现有会话保留已标记的提供商,不会被迁移;新会话仍正常经代理路由。重新标记停手时,主目录里已有的 `[model_providers.opencodex]` 表会保留而不是撤下,即便是 root-override(loopback)形式也一样,这样行上标记为 `opencodex` 的会话仍能对应到还存在的提供商 id。可迁移存储中的 legacy 记录也适用。CLI 会打印 `Codex resume history: left to Codex's native writer (history_paginated_requires_native_writer)`。`ocx restore` 和移除 Codex 配置仍会因 `history_paginated_requires_native_writer` 被拒绝。线程行仍在引用时撤掉 `[model_providers.opencodex]` 定义会使这些会话无法解析,而恢复路径没有办法留下兼容提供商表。已经分页的主目录目前无法通过产品卸载;这是已知的未完成工作,而非预期行为。 +返回根 URL 覆盖模式时,即使历史预检通过,OpenCodex 也会在提交配置前保留已有的 `[model_providers.opencodex]` 定义。这样,即使 Codex 在提交后或后台历史任务启动时迁移历史格式,旧的 `opencodex` 对话仍能找到其提供商。新对话继续使用所选的根提供商;显式恢复仍执行原有的独立删除检查。 + 不要改写正在使用的分页历史文件或线程行来自行迁移这些会话。恢复前关闭相关会话,并只报告准确的错误和版本,不要公开私人历史。备份或脚本成功并不能证明显示已恢复;重新打开 Codex 后检查会话。 diff --git a/docs-site/src/content/docs/zh-tw/guides/codex-integration.md b/docs-site/src/content/docs/zh-tw/guides/codex-integration.md index 284034b54b..f7bfefac13 100644 --- a/docs-site/src/content/docs/zh-tw/guides/codex-integration.md +++ b/docs-site/src/content/docs/zh-tw/guides/codex-integration.md @@ -365,4 +365,6 @@ ocx restore back # 讓普通 Codex 再次指向仍在執行的 proxy 如果受影響的歷史儲存區支援分頁,提供者切換可能傳回 `history_paginated_requires_native_writer`。此原因不再拒絕寫入 Codex 設定、參考設定檔與模型目錄。`ocx sync` 與 `ocx start` 仍會寫入這些檔案並設定 `model_catalog_json`,因此 Codex 模型選擇器會繼續顯示所有經 OpenCodex 路由的模型。只有這一條原因會讓對話歷史的重新標記停手,因為分頁歷史序號由 Codex 自己的寫入器分配,重試也不會改變。無法讀取的狀態資料庫、身分已變的歷史檔案、未能執行的預檢等其他歷史預檢原因仍會拒絕整個切換並回復,因為那些情況以後可能成功。在此狀態下,OpenCodex 不會修改分頁歷史檔案或執行緒列。既有對話保留已標記的提供者,不會被遷移;新對話仍正常經代理路由。重新標記停手時,家目錄裡既有的 `[model_providers.opencodex]` 表會保留而不是撤下,即便是 root-override(loopback)形式也一樣,這樣列上標記為 `opencodex` 的對話仍能對應到還存在的提供者 id。可遷移儲存區中的 legacy 記錄也適用。CLI 會印出 `Codex resume history: left to Codex's native writer (history_paginated_requires_native_writer)`。`ocx restore` 與移除 Codex 設定仍會因 `history_paginated_requires_native_writer` 被拒絕。執行緒列仍在參照時撤掉 `[model_providers.opencodex]` 定義會使這些對話無法解析,而復原路徑沒有辦法留下相容提供者表。已經分頁的家目錄目前無法透過產品解除安裝;這是已知的未完成工作,而非預期行為。 +返回根 URL 覆寫模式時,即使歷史預檢通過,OpenCodex 也會在提交設定前保留既有的 `[model_providers.opencodex]` 定義。如此一來,即使 Codex 在提交後或背景歷史工作啟動時遷移歷史格式,舊的 `opencodex` 對話仍能找到其提供者。新對話繼續使用所選的根提供者;明確要求的還原仍執行原有的獨立刪除檢查。 + 請勿改寫使用中的分頁歷史檔案或執行緒列來自行遷移這些對話。復原前關閉相關對話,只回報確切錯誤與版本,不要公開私人歷史。備份或指令碼成功不能證明顯示已復原;重新開啟 Codex 後確認對話。 diff --git a/src/codex/inject.ts b/src/codex/inject.ts index ea6424e0eb..a01909a9c7 100644 --- a/src/codex/inject.ts +++ b/src/codex/inject.ts @@ -465,9 +465,9 @@ async function injectCodexConfigImpl( */ /* * Re-observed inside the artifact transaction. A store that migrates to paginated history - * mid-write retires the relabel unit, because the config half writes no history and rolling - * it back is what left every paginated home with no OpenCodex models. Any other reason is - * still treated as a failed transition so compensation can restore the pre-images. + * mid-write can retire the relabel unit while its already-admitted candidate leaves + * existing provider references resolvable. Existing provider definitions are retained + * before the witness; no post-commit compensation may overwrite a newer native write. */ const observeHistoryRefusalOrThrow = (known: string | null): string | null => { if (known) return known; @@ -489,12 +489,12 @@ async function injectCodexConfigImpl( /* * Rows this home may have tagged `opencodex` resolve only through a provider table. Design B - * normally retires that table because the relabel migrates those rows back to `openai` in - * the same pass; with the relabel stood down, stripping it anyway would leave every such - * conversation pointing at a provider id that no longer exists. Keep what was already - * published, and keep it BEFORE the witness so the lock admits the bytes actually written. + * selects built-in `openai` for new work, but background relabel and native publication are + * not atomic. Codex can paginate after the final check or when the worker starts. Retain + * an existing definition BEFORE the witness regardless of preflight, so worker failure + * cannot orphan old references. Explicit restoration keeps its removal and history guards. */ - if (historyRelabelRefusal && hadOcxProviderTableOnDisk && !providerTableMode) { + if (hadOcxProviderTableOnDisk && !providerTableMode) { content = applyEol( content.trimEnd() + "\n" + buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {})), eol, @@ -787,6 +787,7 @@ async function injectCodexConfigImpl( // handed down fixed; the Worker never takes a direction from its caller. // A stood-down relabel unit spawns no Worker: the preflight it would run first has // already refused, and the config half is committed either way. + historyArtifactStageForTests?.("before-history-worker"); const historyOutcome: CodexHistoryJobOutcome = historyRelabelRefusal ? { kind: "skipped" } : await runCodexHistoryJob({ @@ -984,4 +985,3 @@ export { setBeforeRestoreConfigForTests, skippedRestoreEnvelope, } from "./inject/restore"; - diff --git a/structure/catalog.md b/structure/catalog.md index 2fd03722df..8307137ce2 100644 --- a/structure/catalog.md +++ b/structure/catalog.md @@ -363,7 +363,7 @@ Provider `showThinkingSummary` is a Responses request default; it does not rewri ## Paginated history writer boundary -`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates detected migration. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. See the [history writer contract](codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. +`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates refused restore/removal transitions. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. Apply retains an existing provider definition before candidate admission even when history preflight passes, so migration after artifact commit or during worker startup cannot leave earlier conversations without their provider. See the [history writer contract](codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. Codex pool settings and their consumers follow the [reset-first ordering contract](providers/openai-tiers.md#reset-first-account-ordering), including independent-quota fallback and preserved affinity. diff --git a/structure/codex-home.md b/structure/codex-home.md index 339358ea9b..aa6b0bb5b3 100644 --- a/structure/codex-home.md +++ b/structure/codex-home.md @@ -252,7 +252,7 @@ Plan-based automatic exclusions leave native credential files untouched and pres Injection preflights affected history using the normalized config candidate before writing config/profile/journal, then checks again after the complete artifact write. Native restore also rechecks after successful journal restoration or fallback removal, while exact config/profile/journal preimages and any coordinated remove transaction remain available for compensation. -What a detected migration does depends on which refusal it is, and on direction. On apply, `history_paginated_requires_native_writer` retires the relabel unit and the config/profile/journal write stands: it is permanent, so compensating it only produced a home with no OpenCodex models at all. Any other reason there — an unreadable state database, a changed rollout identity, a preflight that could not run — may succeed on a later attempt, so it still restores all three preimages before returning a structured refusal, including on legacy-uncoordinated homes. Restore and removal compensate on every reason, because retiring a provider definition its thread rows still name would orphan them. A failed config restore stops catalog/history work; coordinated restore rolls back its published remove transition. Legacy first-line provider patches are bound to the validated file identity before and after writing. These compensating checks do not provide a native-writer lock or authorize external ordinal allocation. +What a detected migration does depends on which refusal it is, and on direction. On apply, `history_paginated_requires_native_writer` retires the relabel unit and the config/profile/journal write stands when the admitted candidate preserves any existing provider table. Retention is decided before witness construction and does not depend on history preflight passing: apply keeps any existing provider definition while selecting the requested root provider. This also protects references when native migration begins after artifact commit or during worker startup, without compensating over newer native writes. Background worker failures remain reported, and candidate bytes never change after admission. Any other reason there — an unreadable state database, a changed rollout identity, a preflight that could not run — may succeed on a later attempt, so it still restores all three preimages before returning a structured refusal, including on legacy-uncoordinated homes. Restore and removal compensate on every reason, because retiring a provider definition its thread rows still name would orphan them. A failed config restore stops catalog/history work; coordinated restore rolls back its published remove transition. Legacy first-line provider patches are bound to the validated file identity before and after writing. These compensating checks do not provide a native-writer lock or authorize external ordinal allocation. The legacy external writer is now refused for affected rows in any store whose schema includes history_mode, even while their row mode is still legacy. This deliberately sacrifices automatic relabeling on migration-capable stores rather than racing native conversion. Synchronous/asynchronous restore, inline journal restore, and direct config removal preserve all artifacts on that refusal, so an already-paginated home cannot yet be uninstalled through the product; apply instead writes its config and keeps a `[model_providers.opencodex]` table the home already published, so rows naming that provider keep resolving. diff --git a/structure/config.md b/structure/config.md index d00b36b06b..780b67275b 100644 --- a/structure/config.md +++ b/structure/config.md @@ -166,8 +166,10 @@ discovery or catalog/cache replacement. Deterministic config and ownership refus leave the existing catalog and cache untouched, and their concrete messages are emitted on stderr. Exactly one conversation-history refusal scopes the relabel unit instead of vetoing the apply transition, and only because it is permanent. Codex allocates paginated rollout ordinals inside -its own writer, so `history_paginated_requires_native_writer` is not retryable: the transition -writes config, profile, and `model_catalog_json`, the relabel job is skipped without spawning +its own writer, so `history_paginated_requires_native_writer` is not retryable: when the admitted +candidate preserves any existing provider table, the transition writes config, profile, +and `model_catalog_json`. +On successful apply, the relabel job is skipped without spawning its Worker, and the reason travels in the human message and in the structured `historyPreflightFailureReason` field *alongside* `success: true`. Every other reason — an unreadable state database, a rollout whose identity changed, a preflight that could not run — @@ -175,12 +177,14 @@ describes a store that may be relabelable on the next attempt, so those keep the and the compensating rollback. Recording them as a stand-down would mark the transition converged and suppress the relabel permanently. -Standing the relabel down changes what the routing form may retire. Rows this home tagged -`opencodex` resolve only through a `[model_providers.opencodex]` table; the loopback form -normally retires that table precisely because the relabel migrates those rows back to `openai` -in the same pass. With the relabel stood down, a table the home already published survives the -write, so those conversations keep a provider id that exists. Paginated rollout bytes and thread -rows are never modified in this state. +Rows this home tagged `opencodex` resolve through a `[model_providers.opencodex]` table. +Apply retains that existing definition before building the candidate witness, even when +history preflight passes. The root-override form still selects the built-in provider for new +conversations. Background history work is not atomic with config publication, so its future +success cannot authorize retiring the old definition first. If native pagination begins after +artifact commit or while the worker starts, the old references still resolve and any worker +failure is reported. Paginated rollout bytes and thread rows remain untouched. Explicit +restore and removal retain their separate guards below. Treating the refusal as a veto is what made every current Codex home unusable: paginated rollouts refuse unconditionally, so `model_catalog_json` never reached config.toml and both the diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index 4628ca5e50..b9da54b46f 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -607,7 +607,7 @@ The provider editor field policy exposes `showThinkingSummary` as a boolean prov ## Paginated history writer boundary -`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates detected migration. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. See the [history writer contract](codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. +`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates refused restore/removal transitions. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. Apply retains an existing provider definition before candidate admission even when history preflight passes, so migration after artifact commit or during worker startup cannot leave earlier conversations without their provider. See the [history writer contract](codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. Codex pool settings and their consumers follow the [reset-first ordering contract](providers/openai-tiers.md#reset-first-account-ordering), including independent-quota fallback and preserved affinity. Codex account DTOs and cards expose the routing-plan exclusion separately from credential health; the [plan exclusion contract](providers/openai-tiers.md#automatic-pool-plan-exclusions) also governs CLI projection. Private pool credential metadata follows the [quota-history publication identity contract](providers/openai-tiers.md#quota-history-publication-identity); credential-only and account DTO projections omit it. diff --git a/structure/ops/docs-and-release.md b/structure/ops/docs-and-release.md index 0c0ffe38d0..4c3293155c 100644 --- a/structure/ops/docs-and-release.md +++ b/structure/ops/docs-and-release.md @@ -353,7 +353,7 @@ Provider configuration documents distinguish actual summaries from raw reasoning ## Paginated history writer boundary -`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates detected migration. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. See the [history writer contract](../codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. +`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates refused restore/removal transitions. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. Apply retains an existing provider definition before candidate admission even when history preflight passes, so migration after artifact commit or during worker startup cannot leave earlier conversations without their provider. See the [history writer contract](../codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. Private pool credential metadata follows the [quota-history publication identity contract](../providers/openai-tiers.md#quota-history-publication-identity); credential-only and account DTO projections omit it. diff --git a/structure/providers/openai-tiers.md b/structure/providers/openai-tiers.md index ab7511c3b0..dcb8d42de7 100644 --- a/structure/providers/openai-tiers.md +++ b/structure/providers/openai-tiers.md @@ -472,7 +472,7 @@ Listener startup diagnostics follow [the runtime lifecycle contract](../runtime. `src/codex/auth-api/account-list.ts` projects `selectionExcludedReason: "plan_excluded"` and `selectionExcludedPlan` from the routing config, even when a newer display-only WHAM plan could not be persisted. The dashboard and account CLI show the policy reason separately from credential health; renewal clears the derived fields. The automatic next-session action and badge are omitted for excluded rows. ## Paginated history writer boundary -`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates detected migration. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. See the [history writer contract](../codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. +`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates refused restore/removal transitions. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. Apply retains an existing provider definition before candidate admission even when history preflight passes, so migration after artifact commit or during worker startup cannot leave earlier conversations without their provider. See the [history writer contract](../codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. diff --git a/structure/runtime.md b/structure/runtime.md index bd9ebbd561..193b06c71c 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -375,7 +375,7 @@ Responses route normalization resolves provider summary defaults from the origin ## Paginated history writer boundary -`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates detected migration. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. See the [history writer contract](codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. +`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates refused restore/removal transitions. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. Apply retains an existing provider definition before candidate admission even when history preflight passes, so migration after artifact commit or during worker startup cannot leave earlier conversations without their provider. See the [history writer contract](codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. Codex pool settings and their consumers follow the [reset-first ordering contract](providers/openai-tiers.md#reset-first-account-ordering), including independent-quota fallback and preserved affinity. diff --git a/structure/subagents.md b/structure/subagents.md index c0c92891f7..cfa7eb29df 100644 --- a/structure/subagents.md +++ b/structure/subagents.md @@ -346,7 +346,7 @@ Final-route summary visibility is recomputed after fallback from the original Re ## Paginated history writer boundary -`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates detected migration. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. See the [history writer contract](codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. +`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates refused restore/removal transitions. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. Apply retains an existing provider definition before candidate admission even when history preflight passes, so migration after artifact commit or during worker startup cannot leave earlier conversations without their provider. See the [history writer contract](codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. Codex pool settings and their consumers follow the [reset-first ordering contract](providers/openai-tiers.md#reset-first-account-ordering), including independent-quota fallback and preserved affinity. diff --git a/tests/codex-integration/codex-inject-integration.test.ts b/tests/codex-integration/codex-inject-integration.test.ts index 5ffbc54f99..1b2d65a1ba 100644 --- a/tests/codex-integration/codex-inject-integration.test.ts +++ b/tests/codex-integration/codex-inject-integration.test.ts @@ -178,6 +178,102 @@ describe("injectCodexConfig integration (Design B)", () => { }); }); + test.each([ + ["before-preflight", false, false], + ["after-preflight", false, false], + ["after-config", false, false], + ["after-artifacts", false, false], + ["after-config", true, false], + ["after-config", false, true], + ] as const)("late pagination preserves an existing provider (%s, coordinated=%s, authless=%s)", (stage, coordinated, authless) => { + const original = 'model_provider="opencodex"\n[model_providers.opencodex]\nname="OpenCodex"\nbase_url="http://127.0.0.1:10100/v1"\nwire_api="responses"\nrequires_openai_auth=true\n'; + const configPath = join(codexHome, "config.toml"); + const profilePath = join(codexHome, "opencodex.config.toml"); + writeFileSync(configPath, original); + if (!coordinated) writeFileSync(profilePath, "# original profile\n"); + if (coordinated) { + writeFileSync(configPath, 'model="test"\n'); + const seed = runInject(codexHome, ocxHome, JSON.stringify({ codexClientCompaction: true })); + expect(seed.status).toBe(0); + expect(JSON.parse(seed.stdout).success).toBe(true); + } + const journalPath = join(codexHome, "opencodex-journal.json"); + const before = [configPath, profilePath, journalPath].map(path => existsSync(path) ? readFileSync(path, "utf8") : null); + const script = ` + const {Database}=require("bun:sqlite"); + const {join}=require("node:path"); + const {injectCodexConfig,setBeforeHistoryArtifactCommitForTests,setHistoryArtifactStageForTests}=require("./src/codex/inject"); + const migrate=()=>{ + const db=new Database(join(process.env.CODEX_HOME,"state_5.sqlite")); + db.run("CREATE TABLE threads (rollout_path TEXT, model_provider TEXT, history_mode TEXT)"); + db.run("INSERT INTO threads VALUES (\'fixture\',\'opencodex\',\'paginated\')"); + db.close(); + }; + let kind; + setBeforeHistoryArtifactCommitForTests(value=>{kind=value;if(${JSON.stringify(stage)}==="before-preflight")migrate();}); + setHistoryArtifactStageForTests(value=>{if(value===${JSON.stringify(stage)})migrate();}); + const readState=${coordinated ? 'require("./src/codex/transition-state").readCodexTransitionState' : "()=>null"}; + const before=readState(); + const result=await injectCodexConfig(10100,{codexDesktopAuthless:${authless}}); + console.log(JSON.stringify({kind,result,before,after:readState()})); + `; + const child=spawnSync(process.execPath,["--eval",script],{cwd:repoRoot,env:{...process.env,CODEX_HOME:codexHome,OPENCODEX_HOME:ocxHome},encoding:"utf8",timeout:SPAWN_BUDGET_MS-5000}); + expect(child.status, child.stderr).toBe(0); + const value = JSON.parse(child.stdout); + expect(value.kind, child.stdout).toBe(coordinated ? "coordinated" : "legacy-uncoordinated"); + expect(value.result).toMatchObject({success:true,historyPreflightFailureReason:"history_paginated_requires_native_writer"}); + const config = Bun.TOML.parse(readFileSync(configPath,"utf8")) as any; + expect(config.model_provider).toBe(authless ? "opencodex" : undefined); + expect(config.model_providers.opencodex.base_url).toBe("http://127.0.0.1:10100/v1"); + expect(readFileSync(profilePath,"utf8")).not.toBe(before[1]); + if (coordinated) expect(value.after).not.toEqual(value.before); + const db = new Database(join(codexHome, "state_5.sqlite"), { readonly: true }); + try { + expect(db.query("SELECT model_provider FROM threads").get()).toEqual({model_provider:"opencodex"}); + } finally { db.close(); } + }); + + test.each([false, true])("pagination after artifact commit keeps the existing provider (coordinated=%s)", coordinated => { + const configPath = join(codexHome, "config.toml"); + writeFileSync(configPath, coordinated ? 'model="test"\n' : DESIGN_B_BLOCK + "\n"); + if (coordinated) { + const seed = runInject(codexHome, ocxHome, JSON.stringify({ codexClientCompaction: true })); + expect(seed.status, seed.stderr).toBe(0); + expect(JSON.parse(seed.stdout).success).toBe(true); + } else { + writeFileSync(configPath, 'model_provider="opencodex"\n[model_providers.opencodex]\nname="OpenCodex"\nbase_url="http://127.0.0.1:10100/v1"\nwire_api="responses"\n'); + writeFileSync(join(codexHome, "opencodex.config.toml"), "# legacy profile\n"); + } + const script = ` + const {Database}=require("bun:sqlite"); + const {join}=require("node:path"); + const {injectCodexConfig,setHistoryArtifactStageForTests}=require("./src/codex/inject"); + let migrated=false; + setHistoryArtifactStageForTests(stage=>{ + if(stage!=="before-history-worker") return; + const db=new Database(join(process.env.CODEX_HOME,"state_5.sqlite")); + db.run("CREATE TABLE threads (rollout_path TEXT, model_provider TEXT, history_mode TEXT)"); + db.run("INSERT INTO threads VALUES ('fixture','opencodex','paginated')"); + db.close();migrated=true; + }); + const result=await injectCodexConfig(10100,{}); + console.log(JSON.stringify({migrated,result})); + `; + const child = spawnSync(process.execPath, ["--eval", script], { + cwd: repoRoot, env: { ...process.env, CODEX_HOME: codexHome, OPENCODEX_HOME: ocxHome }, + encoding: "utf8", timeout: SPAWN_BUDGET_MS - 5_000, + }); + expect(child.status, child.stderr).toBe(0); + const value = JSON.parse(child.stdout); + expect(value.migrated).toBe(true); + const parsed = Bun.TOML.parse(readFileSync(configPath, "utf8")) as any; + expect(parsed.model_providers?.opencodex?.base_url).toBe("http://127.0.0.1:10100/v1"); + expect(value.result.success).toBe(true); + const db = new Database(join(codexHome, "state_5.sqlite"), { readonly: true }); + try { expect(db.query("SELECT model_provider FROM threads").get()).toEqual({ model_provider: "opencodex" }); } + finally { db.close(); } + }); + for (const stage of ["before-preflight", "after-preflight", "after-config", "after-artifacts"]) { test.each([false,true])(`a store that migrates mid-transaction retires the relabel unit and keeps the config (${stage}, legacy=%s)`,(legacy)=>{ const original=legacy ? DESIGN_B_BLOCK+"\n" : 'model="test"\n'; @@ -207,7 +303,7 @@ describe("injectCodexConfig integration (Design B)", () => { const value=JSON.parse(child.stdout); expect(value.kind).toBe(legacy?"legacy-uncoordinated":"coordinated"); // A migration observed at ANY point in the transaction stands the relabel unit down and - // says so. It never rolls the config back: the config half writes no history, and + // says so. With no prior provider table to retire, it need not roll the config back: // rolling it back is what left every paginated home with no OpenCodex models at all. expect(value.result).toMatchObject({success:true,historyPreflightFailureReason:"history_paginated_requires_native_writer"}); expect(value.result.message).toContain("left to Codex's native writer"); @@ -533,11 +629,11 @@ describe("injectCodexConfig integration (Design B)", () => { const config = readFileSync(join(codexHome, "config.toml"), "utf8"); expect(config).toContain('openai_base_url = "http://127.0.0.1:10100/v1"'); expect(config).toContain("# Auto-injected by opencodex"); - expect(config).not.toContain("[model_providers.opencodex]"); + expect(config).toContain("[model_providers.opencodex]"); expect(config).not.toContain('model_provider = "opencodex"'); expect(config).toContain('model = "gpt-5.5"'); - // Exactly the Design B markers survive (routing + realtime sideband) — no accumulation. - expect(config.match(/Auto-injected by opencodex/g)?.length).toBe(2); + // Routing, realtime sideband and the retained compatibility provider each have one marker. + expect(config.match(/Auto-injected by opencodex/g)?.length).toBe(3); expect(config).toContain(DESIGN_B_BLOCK); }); @@ -1261,9 +1357,10 @@ describe("injectCodexConfig integration (Design B)", () => { expect(runInject(codexHome, ocxHome).status).toBe(0); const back = readFileSync(join(codexHome, "config.toml"), "utf8"); expect(back).toContain('openai_base_url = "http://127.0.0.1:10100/v1"'); - expect(back).not.toContain("[model_providers.opencodex]"); + expect(back).toContain("[model_providers.opencodex]"); + expect(back).toContain("requires_openai_auth = true"); expect(back).not.toContain('model_provider = "opencodex"'); - expect(back.match(/Auto-injected by opencodex/g)?.length).toBe(2); + expect(back.match(/Auto-injected by opencodex/g)?.length).toBe(3); expect(back).toContain(DESIGN_B_BLOCK); expect(runInject(codexHome, ocxHome, JSON.stringify({ codexDesktopAuthless: true })).status).toBe(0); @@ -1291,7 +1388,7 @@ describe("injectCodexConfig integration (Design B)", () => { 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).toContain("[model_providers.opencodex]"); expect(designB).not.toContain('model_provider = "opencodex"'); // Disabling leaves exactly one root override, not the table form's copy plus a new one. expect(designB.match(/openai_base_url/g)?.length).toBe(1);