Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# R1 — the paginated-history guard, from activation and from recovery

Scope: #5321 (activation) and #4812 (recovery). Branch `codex/260920-r1-paginated-history-guard`.

## What the guard was actually protecting

`preflightCodexHistoryInjection` returns `history_paginated_openai_requires_native_writer` when a
provider-table transition finds a thread row that is both `model_provider = 'openai'` and
`history_mode = 'paginated'`. The reasoning is sound. The transition takes the root
`openai_base_url` out, a paginated row cannot be relabeled, and Codex builds its provider map as
`merge_configured_model_providers(built_in_model_providers(openai_base_url), model_providers)`, so
without that root line the built-in `openai` entry is `api.openai.com`. The conversation would
resume outside the proxy.

What made it a lockout is that 2.60.0 classified it alongside "something is wrong with this
store". `src/codex/inject.ts` refuses every reason that is not exactly `HISTORY_RELABEL_STANDS_DOWN`,
so nothing was written at all: no config, no profile, no `model_catalog_json`, integration
disabled. Before 2.60.0 the same home returned the plain stand-down, and the routing and catalog
half landed while the relabel stood down.

## The state that was already in the tree

The injector already builds the safe state for one routing form. `keepRootOverrideAlongsideTable`
keeps the marker-owned root override beside the provider table for client compaction, for exactly
this reason, and passes `resumeHistory: false` so the relabel never runs. Authless was excluded
deliberately — its point is `requires_openai_auth = false` — on the assumption that it could
always forward-tag resume history instead. On a paginated home that assumption is false, and the
refusal is where that showed up.

So the fix is not a new mechanism. `src/codex/inject/paginated-openai-compat.ts` selects the
existing one from the preflight verdict rather than from the routing form: when the reason is the
paginated-openai code and the target can own a root key, retain the override, downgrade the reason
to the stand-down constant, and let the transition complete. The paginated row is never read or
written; it simply keeps resolving to this proxy.

Two cases cannot reach that state, and both are honest outcomes rather than traps:

- An admission-token form cannot use the root key at all, because Codex's built-in `openai` entry
carries no `x-opencodex-api-key` header. It keeps the refusal, and the message now names
`unauthenticatedLoopbackListener` and `syncResumeHistory` instead of "do not retry".
- A root line the user owns is left alone. The conversation follows the destination they chose,
which is the guarantee the injector already makes everywhere else about a line it does not own,
and the journal correctly records the line as not ours.

## Where it had to live

`src/codex/inject.ts` was at 984 of its 987-line ratchet cap, so the decision could not be
inlined. The new module costs the injector one import and one net line; the file now sits at
exactly 987. The refusal code became an exported constant in `src/codex/history-provider.ts`
because the same literal in two files is how the stand-down pair drifted the first time.

## #4812, checked rather than assumed

The recovery half is already closed on `dev`: `resolveRestoreHistoryDisposition` stands down on
`HISTORY_RELABEL_STANDS_DOWN` and removal retains the provider table. The new code cannot reach
restore at all — it is only set under `providerTableMode`, and restore preflights with
`providerTableMode = false`, whose row predicate is `model_provider = 'opencodex'`.

Two things were still wrong on that side. `ocx restore --remove-codex-provider-table` existed but
appeared in no usage or help text, so the escape hatch was reachable only by reading the parser;
it is now in the command registry and top-level usage, bound by a test that reads the flag out of
`dispatch.ts` rather than restating it. And the public guide in all eight locales still said
restore and removal refuse on paginated history and that such a home cannot be uninstalled, which
has not been true since 2026-09-17.

## Verification

Static review plus exact-head hosted CI. Per the lane constraints, NOT RUN locally: `bun test`,
any individual test file, `bun run typecheck`, any build, any install, live `ocx`, service
restart, and credential or configuration changes.

Regression coverage added:

- `tests/codex-integration/history-paginated-openai-compat.test.ts` — the resolver itself: root
override retained and placed before the first table, CRLF preserved, a user-owned line left
untouched and not claimed, the admission-token refusal naming both remedies as keys that are
asserted to exist in `src/types/config.ts`, every other reason passing through unchanged, and a
source-oracle check that the refusal code is defined once.
- `tests/codex-integration/codex-inject-integration.test.ts` — the end-to-end regression, rewritten
from "refuses" to the full transition: config carries both the table and the marker-owned root
override, the rollout bytes and the thread row are unchanged, and `ocx restore` afterwards takes
the retained override back out. That last assertion is the one that keeps this from trading
#5321 for a new #4812.
- `tests/cli/cli-restore-back.test.ts` — the removal flag is discoverable in both help surfaces.

## Not in this lane

The other hard-refusal reasons on the recovery side still have no named repair command: a missing
state database with pending manifest entries, and a backup manifest that is unreadable, foreign,
or schema-invalid. Those are a different failure family from the guard and are left open rather
than folded in here.
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ Codex. Seule l'exécution explicite de `ocx stop` ou `ocx service stop` restaure

## Refus de sécurité pour l’historique paginé

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.
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`, `ocx stop` et `ocx uninstall` ne refusent plus sur `history_paginated_requires_native_writer`. Ils retirent toutes les clés de routage racine d'OpenCodex et conservent la définition `[model_providers.opencodex]` sur le disque : les conversations dont les lignes nomment encore ce fournisseur restent résolubles, tandis que `codex` seul cesse de pointer vers le proxy. Le résultat est signalé comme une restauration partielle qui nomme les lignes conservées, et `ocx restore --remove-codex-provider-table` les supprime aussi, après quoi ces conversations ne s'ouvrent plus. Par ailleurs, activer l'intégration sous sa forme table de fournisseur sur un répertoire d'accueil dont les conversations marquées `openai` ont déjà été paginées par Codex était auparavant refusé d'emblée avec `history_paginated_openai_requires_native_writer` : rien n'était écrit et l'intégration restait désactivée. OpenCodex termine désormais cette transition en conservant la redéfinition racine gérée `openai_base_url` à côté de la table `[model_providers.opencodex]`. Codex fusionne cette redéfinition avec son fournisseur `openai` intégré, donc ces conversations continuent d'atteindre le proxy sans être réétiquetées, et aucun octet d'historique ni ligne de conversation n'est modifié. Seule une forme de routage exigeant l'en-tête d'admission `x-opencodex-api-key` refuse encore, car le fournisseur intégré de Codex ne peut pas porter cet en-tête ; son message nomme les deux réglages qui résolvent la situation — router Codex par l'écouteur loopback pour conserver la redéfinition, ou mettre `syncResumeHistory` à `false` en acceptant que ces conversations reprennent sur le point de terminaison OpenAI propre à Codex.

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.

Expand Down
4 changes: 3 additions & 1 deletion docs-site/src/content/docs/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -919,7 +919,9 @@ When an affected history store supports paginated records, a provider transition

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.
`ocx restore`, `ocx stop` and `ocx uninstall` no longer refuse on `history_paginated_requires_native_writer`. They take every OpenCodex root routing key out and keep the `[model_providers.opencodex]` definition on disk, so conversations whose rows still name that provider keep resolving while plain `codex` stops pointing at the proxy. The result is reported as a partial restore that names the retained lines, and `ocx restore --remove-codex-provider-table` removes them too, after which those conversations stop opening.

Enabling the integration in its provider-table form on a home whose `openai`-tagged conversations Codex has already paginated used to be refused outright with `history_paginated_openai_requires_native_writer`: nothing was written and the integration stayed disabled. OpenCodex now completes that transition by keeping the managed root `openai_base_url` override beside the `[model_providers.opencodex]` table. Codex merges the override onto its built-in `openai` provider, so those conversations keep reaching the proxy without being relabeled and no rollout byte or thread row is touched. Only a routing form that requires the `x-opencodex-api-key` admission header still refuses, because Codex's built-in provider cannot carry that header; its message names the two settings that resolve it — route Codex through the loopback listener so the override can be retained, or set `syncResumeHistory` to `false` to accept that those conversations resume against Codex's own OpenAI endpoint.

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.

Expand Down
Loading
Loading