Support reliable cross-provider session handoff with normalized tool history

Open 💬 6 comments Opened Aug 13, 2026 by haochengyuan

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

  1. codex resume across providers is unreliable because the session retains its

original provider-specific state.

  1. codex fork -p <provider> -m <model> <session-id> copies the history, but the

destination provider may reject valid historical tool records.

  1. In my case DeepSeek returned:

No tool output found for tool call call_mj42PDsUwFvbo5vPJM4Kunyd

  1. The corresponding custom_tool_call_output was 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.

  1. 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_

View original on GitHub ↗

6 Comments

dajiaohuang · 14 days ago

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:

  • resume retains 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.
  • fork creates 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.
  • Local resume/fork discovery is filtered to the active provider, while direct UUID lookup bypasses that listing filter.
  • Prompt-history normalization currently supplies missing tool outputs and removes orphan outputs, but does not reorder an existing call/output pair around an intervening developer message.
  • The persisted response-item model also contains provider-sensitive fields/items (for example encrypted reasoning/function data, item IDs, additional tools, agent messages, tool-search and compaction records). The current non-OpenAI request preparation only sanitizes a limited subset.

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:

  1. For parallel/interleaved tool calls, should the portable invariant be adjacent call/output pairs, or a group of calls followed by the corresponding group of outputs? Where should intervening developer messages land?
  2. Which provider-specific records should be retained, transformed, omitted, or rejected (especially encrypted reasoning/compaction, agent messages, additional tools, tool-search items, response IDs, and namespaces)?
  3. Should a normalized child prefix be materialized, or should the child retain source-backed history and persist a normalization policy/version for cold resume?
  4. Is provider-independent session discovery part of this issue, or should it remain separate from handoff normalization?

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.

dajiaohuang · 14 days ago

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.

CCanxue · 14 days ago

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-fix

jdcodes1 · 9 days ago

Half of the normalization layer you're proposing already exists — which sharpens what's actually missing. core/src/context_manager/normalize.rs guarantees presence invariants when history is rebuilt: ensure_call_outputs_present synthesizes an output for any dangling call, remove_orphan_outputs drops 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_content only 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.

dajiaohuang · 9 days ago

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:

  • source-provider response IDs and encrypted function arguments are cleared;
  • provider reasoning and compaction records become bounded developer-visible placeholders without exposing hidden reasoning;
  • encrypted inter-agent messages and encrypted tool output are omitted behind explicit placeholders;
  • supported call/output pairs remain adjacent; and
  • an integration test now verifies that the projected prefix is persisted into the fork for cold resume.

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_provider passes both focused tests. The branch was previously checked with just fix -p codex-core and just fmt.

vshulcz · 4 days ago

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_text is 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.