Support reliable cross-provider session handoff with normalized tool history
What variant of Codex are you using?
CLI
What feature would you like to see?
Feature request
Codex should support handing off a long-running session from one model provider
to another while preserving its working context.
Example:
- Continue a long GPT session with a custom Responses-compatible provider when
the GPT quota is exhausted.
- Later hand the resulting session back to OpenAI models.
- Preserve completed conversation history, decisions, tool results, and project
context without requiring a manual summary.
### Current behavior
codex resumeacross providers is unreliable because the session retains its
original provider-specific state.
codex fork -p <provider> -m <model> <session-id>copies the history, but the
destination provider may reject valid historical tool records.
- In my case DeepSeek returned:
No tool output found for tool call call_mj42PDsUwFvbo5vPJM4Kunyd
- The corresponding
custom_tool_call_outputwas present in the rollout file,
but a developer message occurred between the tool call and its output.
OpenAI accepted this history; the custom provider rejected it.
- Provider-specific pickers also make the source session difficult to locate,
requiring its UUID.
### Desired behavior
Before a cross-provider resume or fork, Codex should normalize the transcript
into a provider-neutral representation:
- preserve all user and assistant messages;
- preserve completed tool calls and outputs;
- guarantee valid call/output pairing and ordering;
- remove or transform provider-specific protocol items;
- retain the source session as an unchanged rollback point;
- clearly indicate that the destination is a new session derived from the
original session.
Ideally:
codex handoff <session-id> --profile deepseek --model deepseek-v4-pro
### Environment
- Codex CLI 0.147.0
- macOS
- Source provider: OpenAI
- Destination: custom Responses-compatible provider
Additional information
_No response_
6 Comments
I did a read-only trace of the current resume/fork and history paths on
main.For account context, I use Codex on a Pro 20x subscription.
Current behavior appears to be:
resumeretains the original conversation ID and appends to the existing rollout, so using it as a cross-provider handoff would not preserve the source as a rollback point.forkcreates a new conversation ID, records source lineage, and freezes the source history boundary. That seems like the existing primitive that best matches the requested rollback semantics.A bounded first implementation could therefore use the existing fork path, with no new CLI/API: create a new child, leave the source rollout and metadata unchanged, and apply a deterministic/idempotent destination-provider projection to the child's model-visible history. Same-provider resume/fork behavior would remain unchanged.
Before code, I think the following semantics need maintainer direction:
Suggested tests would cover the reported call -> developer message -> output sequence, parallel calls, missing/orphan outputs, OpenAI <-> custom-provider forks through mock Responses servers, new child/source lineage, unchanged source rollout, cold child resume, idempotence, and same-provider regressions.
I will wait for maintainer guidance and an explicit contribution invitation before making any code changes.
Policy update (2026-08-21): the repository now states that it does not accept external code contributions or pull requests. The tested fork Draft remains available only as a technical reference; I am not requesting an invitation or upstream submission.
The reference is https://github.com/dajiaohuang/codex/pull/2. It is a bounded first stage: cross-provider forks deterministically pair supported calls with their outputs, materialize the projected child prefix, and leave the source and same-provider paths unchanged. The regression covers the reported interleaving and idempotence. The draft explicitly lists provider-specific records and discovery changes that remain out of scope.
Related evidence: cross-provider history replay is already fragile in this family. DeepSeek stores plaintext
reasoning_text/agent-message shapes that strict OpenAI endpoints reject on switch (see hermes-agent#32617, openai/codex#17541), and the reverse direction fails too. A normalized, provider-neutral transcript before resume/fork matches the direction several of these issues ask for; our patch only covers task delivery, not history normalization. Family overview: https://github.com/CCanxue/codex-deepseek-subagent-fixHalf of the normalization layer you're proposing already exists — which sharpens what's actually missing.
core/src/context_manager/normalize.rsguarantees presence invariants when history is rebuilt:ensure_call_outputs_presentsynthesizes an output for any dangling call,remove_orphan_outputsdrops outputs with no call, and there are strip passes for unsupported media (https://github.com/openai/codex/blob/1f41cc5d92/codex-rs/core/src/context_manager/normalize.rs#L21,#L148).What it never enforces is adjacency: your DeepSeek failure is a call/output pair separated by an interleaved developer message — valid for the OpenAI Responses API, rejected by stricter chat-completions-style validators. That's a pure reordering transform the existing pass could add. The second gap is provider-opaque items: encrypted reasoning payloads and inter-agent messages carry
encrypted_contentonly the originating provider can use (#38656 documents a fork of this problem), and compaction markers are protocol-specific — those need dropping or flattening to text on a provider switch.So the feature could land incrementally: extend the existing normalize pass with adjacency + opaque-item handling, and run it on
fork -p/cross-provider resume. No new transcript format needed.Thanks for the additional cross-provider evidence and for pointing to context_manager/normalize.rs.
I updated the fork-only Draft at https://github.com/dajiaohuang/codex/pull/2 to incorporate the opaque-item side of that feedback:
I kept this projection at the cross-provider fork boundary rather than adding adjacency to the global prompt normalization pass. The existing pass is provider-agnostic and only normalizes the prompt view; it does not durably rewrite the child history, so a resumed cross-provider fork could otherwise recover the original provider-specific ordering and opaque records.
Validation after rebasing to current upstream main (88c39c457):
just test -p codex-core cross_providerpasses both focused tests. The branch was previously checked withjust fix -p codex-coreandjust fmt.A different framing that sidesteps the rejection problem rather than normalising around it, from having built handoff across twenty agents.
Do not move the tool history at all. What the destination needs in order to continue is not the provider-specific record of how the work happened — it is the work: the problem being solved, the conclusions reached so far, and where it stopped. That is plain text, and no endpoint rejects plain text. The transcript on disk already holds it, so the package can be rebuilt from the source session instead of converted from the previous provider's wire format.
The trade is explicit and worth stating: you lose replay of tool calls as tool calls, so the new session starts as a fresh conversation that happens to know what the old one concluded. What you gain is that the move works between providers sharing no schema at all — and between different agents, not only different providers behind one agent.
The failure in the report is the general case of the alternative: a normalisation layer has to keep up with every provider's record shape, forever, and
reasoning_textis only the instance that surfaced this week. Text has no such obligation.Ours is
deja handoff --to <agent>in https://github.com/vshulcz/deja-vu, Codex included. If it is useful I can write up which parts of a session actually turned out to matter on the other side and which we stopped carrying.